diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md
index 9b1df6b59c..fec40b1e9c 100644
--- a/docs/ENVIRONMENT.md
+++ b/docs/ENVIRONMENT.md
@@ -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
diff --git a/docs/GROUP_RECORD.md b/docs/GROUP_RECORD.md
new file mode 100644
index 0000000000..8f98b49c27
--- /dev/null
+++ b/docs/GROUP_RECORD.md
@@ -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"
+ }
+ }
+}
+```
diff --git a/docs/USER_GROUP_API.md b/docs/USER_GROUP_API.md
new file mode 100644
index 0000000000..21d498f5fd
--- /dev/null
+++ b/docs/USER_GROUP_API.md
@@ -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.
diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md
new file mode 100644
index 0000000000..a12870dd42
--- /dev/null
+++ b/docs/USER_RECORD.md
@@ -0,0 +1,1023 @@
+---
+title: JSON User Records
+category: Interfaces
+layout: default
+---
+
+# JSON User Records
+
+systemd optionally processes user records that go beyond the classic UNIX (or
+glibc NSS) `struct passwd`. Various components of systemd are able to provide
+and consume records in a more extensible format of a dictionary of key/value
+pairs, encoded as JSON. Specifically:
+
+1. [`systemd-homed.service`](https://www.freedesktop.org/software/systemd/man/systemd-homed.service.html)
+ manages `human` user home directories and embeds these JSON records
+ directly in the home directory images (see [Home
+ Directories](https://systemd.io/HOME_DIRECTORY)) for details.
+
+2. [`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html)
+ processes these JSON records for users that log in, and applies various
+ settings to the activated session, including environment variables, nice
+ levels and more.
+
+3. [`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html)
+ processes these JSON records of users that log in, and applies various
+ resource management settings to the per-user slice units it manages. This
+ allows setting global limits on resource consumption by a specific user.
+
+4. [`nss-systemd`](https://www.freedesktop.org/software/systemd/man/nss-systemd.html)
+ is a glibc NSS module that synthesizes classic NSS records from these JSON
+ records, providing full backwards compatibility with the classic UNIX APIs
+ both for look-up and enumeration.
+
+5. The service manager (PID 1) exposes dynamic users (i.e. users synthesized as
+ effect of `DynamicUser=` in service unit files) as these advanced JSON
+ records, making them discoverable to the rest of the system.
+
+6. [`systemd-userdbd.service`](https://www.freedesktop.org/software/systemd/man/systemd-userdbd.service.html)
+ is a small service that can translate UNIX/glibc NSS records to these JSON
+ user records. It also provides a unified [Varlink](https://varlink.org/) API
+ for querying and enumerating records of this type, optionally acquiring them
+ from various other services.
+
+JSON user records may contain various fields that are not available in `struct
+passwd`, and are extensible for other applications. For example, the record may
+contain information about:
+
+1. Additional security credentials (PKCS#11 security token information,
+ biometrical authentication information, SSH public key information)
+
+2. Additional user metadata, such as a picture, email address, location string,
+ preferred language or timezone
+
+3. Resource Management settings (such as CPU/IO weights, memory and tasks
+ limits, classic UNIX resource limits or nice levels)
+
+4. Runtime parameters such as environment variables or the `nodev`, `noexec`,
+ `nosuid` flags to use for the home directory
+
+5. Information about where to mount the home directory from
+
+And various other things. The record is intended to be extensible, for example
+the following extensions are envisioned:
+
+1. Windows network credential information
+
+2. Information about default IMAP, SMTP servers to use for this user
+
+3. Parental control information to enforce on this user
+
+4. Default parameters for backup applications and similar
+
+Similar to JSON User Records there are also [JSON Group
+Records](https://systemd.io/GROUP_RECORD.md) that encapsulate UNIX groups.
+
+JSON User Records may be transferred or written to disk in various protocols
+and formats. To inquire about such records defined on the local system use the
+[User/Group Lookup API via Varlink](https://systemd.io/USER_GROUP_API.md).
+
+## Why JSON?
+
+JSON is nicely extensible and widely used. In particular it's easy to
+synthesize and process with numerous programming languages. It's particularly
+popular in the web communities, which hopefully should make it easy to link
+user credential data from the web and from local systems more closely together.
+
+## General Structure
+
+The JSON user records generated and processed by systemd follow a general
+structure, consisting of seven distinct "sections". Specifically:
+
+1. Various fields are placed at the top-level of user record (the `regular`
+ section). These are generally fields that shall apply unconditionally to the
+ user in all contexts, are portable and not security sensitive.
+
+2. A number of fields are located in the `privileged` section (a sub-object of
+ the user record). Fields contained in this object are security sensitive,
+ i.e. contain information that the user and the administrator should be able
+ to see, but other users should not. In many ways this matches the data
+ stored in `/etc/shadow` in classic Linux user accounts, i.e. includes
+ password hashes and more. Algorithmically, when a user record is passed to
+ an untrusted client, by monopolizing such sensitive records in a single
+ object field we can easily remove it from view.
+
+3. A number of fields are located in objects inside the `perMachine` section
+ (an array field of the user record). Primarily these are resource
+ management-related fields, as those tend to make sense on a specific system
+ only, e.g. limiting a user's memory use to 1G only makes sense on a specific
+ system that has more than 1G of memory. Each object inside the `perMachine`
+ array comes with a `matchMachineId` or `matchHostname` field which indicate
+ which systems to apply the listed settings to. Note that many fields
+ accepted in the `perMachine` section can also be set at the top level (the
+ `regular` section), where they define the fallback if no matching object in
+ `perMachine` is found.
+
+4. Various fields are located in the `binding` section (a sub-sub-object of the
+ user record; an intermediary object is inserted which is keyed by the
+ machine ID of the host). Fields included in this section "bind" the object
+ to a specific system. They generally include non-portable information about
+ paths or UID assignments, that are true on a specific system, but not
+ necessarily on others, and which are managed automatically by some user
+ record manager (such as `systemd-homed`). Data in this section is considered
+ part of the user record only in the local context, and is generally not
+ ported to other systems. Due to that it is not included in the reduced user
+ record the cryptographic signature defined in the `signature` section is
+ calculated on. In `systemd-homed` this section is also removed when the
+ user's record is stored in the `~/.identity` file in the home directory, so
+ that every system with access to the home directory can manage these
+ `binding` fields individually. Typically, the binding section is persisted
+ to the local disk.
+
+5. Various fields are located in the `status` section (a sub-sub-object of the
+ user record, also with an intermediary object between that is keyed by the
+ machine ID, similar to the way the `binding` section is organized). This
+ section is augmented during runtime only, and never persisted to disk. The
+ idea is that this section contains information about current runtime
+ resource usage (for example: currently used disk space of the user), that
+ changes dynamically but is otherwise immediately associated with the user
+ record and for many purposes should be considered to be part of the user
+ record.
+
+6. The `signature` section contains one or more cryptographic signatures of a
+ reduced version of the user record. This is used to ensure that only user
+ records defined by a specific source are accepted on a system, by validating
+ the signature against the set of locally accepted signature public keys. The
+ signature is calculated from the JSON user record with all sections removed,
+ except for `regular`, `privileged`, `perMachine`. Specifically, `binding`,
+ `status`, `signature` itself and `secret` are removed first and thus not
+ covered by the signature. This section is optional, and is only used when
+ cryptographic validation of user records is required (as it is by
+ `systemd-homed.service` for example).
+
+7. The `secret` section contains secret user credentials, such as password or
+ PIN information. This data is never persisted, and never returned when user
+ records are inquired by a client, privileged or not. This data should only
+ be included in a user record very briefly, for example when certain very
+ specific operations are executed. For example, in tools such as
+ `systemd-homed` this section may be included in user records, when creating
+ a new home directory, as passwords and similar credentials need to be
+ provided to encrypt the home directory with.
+
+Here's a tabular overview of the sections and their properties:
+
+| Section | Included in Signature | Persistent | Security Sensitive | Contains Host-Specific Data |
+|------------|-----------------------|------------|--------------------|-----------------------------|
+| regular | yes | yes | no | no |
+| privileged | yes | yes | yes | no |
+| perMachine | yes | yes | no | yes |
+| binding | no | yes | no | yes |
+| status | no | no | no | yes |
+| signature | no | yes | no | no |
+| secret | no | no | yes | no |
+
+Note that services providing user records to the local system are free to
+manage only a subset of these sections and never include the others in
+them. For example, a service that has no concept of signed records (for example
+because the records it manages are inherently trusted anyway) does not have to
+bother with the `signature` section. A service that only defines records in a
+strictly local context and without signatures doesn't have to deal with the
+`perMachine` or `binding` sections and can include its data exclusively in the
+regular section. A service that uses a separate, private channel for
+authenticating users (or that doesn't have a concept of authentication at all)
+does not need to to be concerned with the `secret` section of user records, as
+the fields included therein are only useful when executing authentication
+operations natively against JSON user records.
+
+The `systemd-homed' manager uses all seven sections for various
+purposes. Inside the home directories (and if the LUKS2 backend is used, also
+in the LUKS2 header) a user record containing the `regular`, `privileged`,
+`perMachine` and `signature` sections is stored. `systemd-homed` also stores a
+version of the record on the host, with the same four sections and augmented
+with an additional, fifth `binding` section. When a local client enquires about
+a user record managed by `systemd-homed` the service will add in some
+additional information about the user and home directory in the `status`
+section — this version is only transferred via IPC and never written to
+disk. Finally the `secret` section is used during authentication operations via
+IPC to transfer the user record along with its authentication tokens in one go.
+
+## Fields in the `regular` section
+
+As mentioned, the `regular` section's fields are placed at the top level
+object. The following fields are currently defined:
+
+`userName` → The UNIX user name for this record. Takes a string with a valid
+UNIX user name. This field is the only mandatory field, all others are
+optional. Corresponds with the `pw_name` field of of `struct passwd` and the
+`sp_namp` field of `struct spwd` (i.e. the shadow user record stored in
+`/etc/shadow`).
+
+`realm` → The "realm" a user is defined in. This concept allows distinguishing
+users with the same name that originate in different organizations or
+installations. This should take a string in DNS domain syntax, but doesn't have
+to refer to an actual DNS domain (though it is recommended to use one for
+this). The idea is that the user `lpoetter` in the `redhat.com` realm might be
+distinct from the same user in the `poettering.hq` realm. User records for the
+same user name that have different realm fields are considered referring to
+different users. When updating a user record it is required that any new
+version has to match in both `userName` and `realm` field. This field is
+optional, when unset the user should not be considered part of any realm. A
+user record with a realm set is never compatible (for the purpose of updates,
+see above) with a user record without one set, even if the `userName` field matches.
+
+`realName` → The real name of the user, a string. This should contain the user's
+real ("human") name, and corresponds loosely to the GECOS field of classic UNIX
+user records. When converting a `struct passwd` to a JSON user record this
+field is initialized from GECOS (i.e. the `pw_gecos` field), and vice versa
+when converting back. That said, unlike GECOS this field is supposed to contain
+only the real name and no other information.
+
+`emailAddress` → The email address of the user, formatted as
+string. [`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html)
+initializes the `$EMAIL` environment variable from this value for all login
+sessions.
+
+`iconName` → The name of an icon picked by the user, for example for the
+purpose of an avatar. This must be a string, and should follow the semantics
+defined in the [Icon Naming
+Specification](https://standards.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html).
+
+`location` → A free-form location string describing the location of the user,
+if that is applicable. It's probably wise to use a location string processable
+by geo-location subsystems, but this is not enforced nor required. Example:
+`Berlin, Germany` or `Basement, Room 3a`.
+
+`disposition` → A string, one of `intrinsic`, `system`, `dynamic`, `regular`,
+`container`, `reserved`. If specified clarifies the disposition of the user,
+i.e. the context it is defined in. For regular, "human" users this should be
+`regular`, for system users (i.e. users that system services run under, and
+similar) this should be `system`. The `intrinsic` disposition should be used
+only for the two users that have special meaning to the OS kernel itself,
+i.e. the `root` and `nobody` users. The `container` string should be used for
+users that are used by an OS container, and hence will show up in `ps` listings
+and such, but are only defined in container context. Finally `reserved` should
+be used for any users outside of these use-cases. Note that this property is
+entirely optional and applications are assumed to be able to derive the
+disposition of a user automatically from a record even in absence of this
+field, based on other fields, for example the numeric UID. By setting this
+field explicitly applications can override this default determination.
+
+`lastChangeUSec` → An unsigned 64bit integer value, referring to a timestamp in µs
+since the epoch 1970, indicating when the user record (specifically, any of the
+`regular`, `privileged`, `perMachine` sections) was last changed. This field is
+used when comparing two records of the same user to identify the newer one, and
+is used for example for automatic updating of user records, where appropriate.
+
+`lastPasswordChangeUSec` → Similar, also an unsigned 64bit integer value,
+indicating the point in time the password (or any authentication token) of the
+user was last changed. This corresponds to the `sp_lstchg` field of `struct
+spwd`, i.e. the matching field in the user shadow database `/etc/shadow`,
+though provides finer resolution.
+
+`shell` → A string, referring to the shell binary to use for terminal logins of
+this user. This corresponds with the `pw_shell` field of `struct passwd`, and
+should contain an absolute file system path. For system users not suitable for
+terminal log-in this field should not be set.
+
+`umask` → The `umask` to set for the user's login sessions. Takes an
+integer. Note that usually on UNIX the umask is noted in octal, but JSON's
+integers are generally written in decimal, hence in this context we denote it
+umask in decimal too. The specified value should be in the valid range for
+umasks, i.e. 0000…0777 (in octal as typical in UNIX), or 0…511 (in decimal, how
+it actually appears in the JSON record). This `umask` is automatically set by
+[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html)
+for all login sessions of the user.
+
+`environment` → An array of strings, each containing an environment variable
+and its value to set for the user's login session, in a format compatible with
+[`putenv()`](http://man7.org/linux/man-pages/man3/putenv.3.html). Any
+environment variable listed here is automatically set by
+[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html)
+for all login sessions of the user.
+
+`timeZone` → A string indicating a preferred timezone to use for the user. When
+logging in
+[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html)
+will automatically initialize the `$TZ` environment variable from this
+string. The string should be a `tzdata` compatible location string, for
+example: `Europe/Berlin`.
+
+`preferredLanguage` → A string indicating the preferred language/locale for the
+user. When logging in
+[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html)
+will automatically initialize the `$LANG` environment variable from this
+string. The string hence should be in a format compatible with this environment
+variable, for example: `de_DE.UTF8`.
+
+`niceLevel` → An integer value in the range -20…19. When logging in
+[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html)
+will automatically initialize the login process' nice level to this value with,
+which is then inherited by all the user's processes, see
+[`setpriority()`](http://man7.org/linux/man-pages/man2/setpriority.2.html) for
+more information.
+
+`resourceLimits` → An object, where each key refers to a Linux resource limit
+(such as `RLIMIT_NOFILE` and similar). Their values should be an object with
+two keys `cur` and `max` for the soft and hard resource limit. When logging in
+[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html)
+will automatically initialize the login process' resource limits to these
+values, which is then inherited by all the user's processes, see
+[`setrlimit()`](http://man7.org/linux/man-pages/man2/setrlimit.2.html) for more
+information.
+
+`locked` → A boolean value. If true the user account is locked, the user may
+not log in. If this field is missing it should be assumed to be false,
+i.e. logins are permitted. This field corresponds to the `sp_expire` field of
+`struct spwd` (i.e. the `/etc/shadow` data for a user) being set to zero or
+one.
+
+`notBeforeUSec` → An unsigned 64bit integer value, indicating a time in µs since
+the UNIX epoch (1970) before which the record should be considered invalid for
+the purpose of logging in.
+
+`notAfterUSec` → Similar, but indicates the point in time *after* which logins
+shall not be permitted anymore. This corresponds to the `sp_expire` field of
+`struct spwd`, when it is set to a value larger than one, but provides finer
+granularity.
+
+`storage` → A string, one of `classic`, `luks`, `directory`, `subvolume`,
+`fscrypt`, `cifs`. Indicates the storage mechanism for the user's home
+directory. If `classic` the home directory is a plain directory as in classic
+UNIX. When `directory`, the home directory is a regular directory, but the
+`~/.identity` file in it contains the user's user record, so that the directory
+is self-contained. Similar, `subvolume` is a `btrfs` subvolume that also
+contains a `~/.identity` user record; `fscrypt` is an `fscrypt`-encrypted
+directory, also containing the `~/.identity` user record; `luks` is a per-user
+LUKS volume that is mounted as home directory, and `cifs` a home directory
+mounted from a Windows File Share. The five latter types are primarily used by
+`systemd-homed` when managing home directories, but may be used if other
+managers are used too. If this is not set `classic` is the implied default.
+
+`diskSize` → An unsigned 64bit integer, indicating the intended home directory
+disk space in bytes to assign to the user. Depending on the selected storage
+type this might be implement differently: for `luks` this is the intended size
+of the file system and LUKS volume, while for the others this likely translates
+to classic file system quota settings.
+
+`diskSizeRelative` → Similar to `diskSize` but takes a relative value, but
+specifies a fraction of the available disk space on the selected storage medium
+to assign to the user. This unsigned integer value is normalized to 2^32 =
+100%.
+
+`skeletonDirectory` → Takes a string with the absolute path to the skeleton
+directory to populate a new home directory from. This is only used when a home
+directory is first created, and defaults to `/etc/skel` if not defined.
+
+`accessMode` → Takes an unsigned integer in the range 0…511 indicating the UNIX
+access mask for the home directory when it is first created.
+
+`tasksMax` → Takes an unsigned 64bit integer indicating the maximum number of
+tasks the user may start in parallel during system runtime. This value is
+enforced on all tasks (i.e. processes and threads) the user starts or that are
+forked off these processes regardless if the change user identity (for example
+by setuid binaries/`su`/`sudo` and
+similar). [`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html)
+enforces this by setting the `TasksMax` slice property for the user's slice
+`user-$UID.slice`.
+
+`memoryHigh`/`memoryMax` → These take unsigned 64bit integers indicating upper
+memory limits for all processes of the user (plus all processes forked off them
+that might have changed user identity), in bytes. Enforced by
+[`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html),
+similar to `tasksMax`.
+
+`cpuWeight`/`ioWeight` → These take unsigned integers in the range 100…10000
+and configure the CPU and IO scheduling weights for the user's processes as a
+whole. Also enforced by
+[`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html),
+similar to `tasksMax`, `memoryHigh` and `memoryMax`.
+
+`mountNoDevices`/`mountNoSuid`/`mountNoExecute` → Three booleans that control
+the `nodev`, `nosuid`, `noexec` mount flags of the user's home
+directories. Note that these booleans are only honored if the home directory
+is managed by a subsystem such as `systemd-homed.service` that automatically
+mounts home directories on login.
+
+`cifsDomain` → A string indicating the Windows File Sharing domain (CIFS) to
+use. This is generally useful, but particularly when `cifs` is used as storage
+mechanism for the user's home directory, see above.
+
+`cifsUserName` → A string indicating the Windows File Sharing user name (CIFS)
+to associate this user record with. This is generally useful, but particularly
+useful when `cifs` is used as storage mechanism for the user's home directory,
+see above.
+
+`cifsService` → A string indicating the Windows File Share service (CIFS) to
+mount as home directory of the user on login.
+
+`imagePath` → A string with an absolute file system path to the file, directory
+or block device to use for storage backing the home directory. If the `luks`
+storage is used this refers to the loopback file or block device node to store
+the LUKS volume on. For `fscrypt`, `directory`, `subvolume` this refers to the
+directory to bind mount as home directory on login. Not defined for `classic`
+or `cifs`.
+
+`homeDirectory` → A string with an absolute file system path to the home
+directory. This is where the image indicated in `imagePath` is mounted to on
+login and thus indicates the application facing home directory while the home
+directory is active, and is what the user's `$HOME` environment variable is set
+to during log-in. It corresponds to the `pw_dir` field of `struct passwd`.
+
+`uid` → An unsigned integer in the range 0…4294967295: the numeric UNIX user ID (UID) to
+use for the user. This corresponds to the `pw_uid` field of `struct passwd`.
+
+`gid` → An unsigned integer in the range 0…4294967295: the numeric UNIX group
+ID (GID) to use for the user. This corresponds to the `pw_gid` field of
+`struct passwd`.
+
+`memberOf` → An array of strings, each indicating a UNIX group this user shall
+be a member of. The listed strings must be valid group names, but it is not
+required that all groups listed exist in all contexts: any entry for which no
+group exists should be silently ignored.
+
+`fileSystemType` → A string, one of `ext4`, `xfs`, `btrfs` (possibly others) to
+use as file system for the user's home directory. This is primarily relevant
+when the storage mechanism used is `luks` as a file system to use inside the
+LUKS container must be selected.
+
+`partitionUuid` → A string containing a lower-case, text-formatted UUID, referencing
+the GPT partition UUID the home directory is located in. This is primarily
+relevant when the storage mechanism used is `luks`.
+
+`luksUuid` → A string containing a lower-case, text-formatted UUID, referencing
+the LUKS volume UUID the home directory is located in. This is primarily
+relevant when the storage mechanism used is `luks`.
+
+`fileSystemUuid` → A string containing a lower-case, text-formatted UUID,
+referencing the file system UUID the home directory is located in. This is
+primarily relevant when the storage mechanism used is `luks`.
+
+`luksDiscard` → A boolean. If true and `luks` storage is used controls whether
+the loopback block devices, LUKS and the file system on top shall be used in
+`discard` mode, i.e. erased sectors should always be returned to the underlying
+storage. If false and `luks` storage is used turns this behavior off. In
+addition, depending on this setting an `FITRIM` or `fallocate()` operation is
+executed to make sure the image matches the selected option.
+
+`luksCipher` → A string, indicating the cipher to use for the LUKS storage mechanism.
+
+`luksCipherMode` → A string, selecting the cipher mode to use for the LUKS storage mechanism.
+
+`luksVolumeKeySize` → An unsigned integer, indicating the volume key length in
+bytes to use for the LUKS storage mechanism.
+
+`luksPbkdfHashAlgorithm` → A string, selecting the hash algorithm to use for
+the PBKDF operation for the LUKS storage mechanism.
+
+`luksPbkdfType` → A string, indicating the PBKDF type to use for the LUKS storage mechanism.
+
+`luksPbkdfTimeCostUSec` → An unsigned 64bit integer, indicating the intended
+time cost for the PBKDF operation, when the LUKS storage mechanism is used, in
+µs.
+
+`luksPbkdfMemoryCost` → An unsigned 64bit integer, indicating the intended
+memory cost for the PBKDF operation, when LUKS storage is used, in bytes.
+
+`luksPbkdfParallelThreads` → An unsigned 64bit integer, indicating the intended
+required parallel threads for the PBKDF operation, when LUKS storage is used.
+
+`service` → A string declaring the service that defines or manages this user
+record. It is recommended to use reverse domain name notation for this. For
+example, if `systemd-homed` manages a user a string of `io.systemd.Home` is
+used for this.
+
+`rateLimitIntervalUSec` → An unsigned 64bit integer that configures the
+authentication rate limiting enforced on the user account. This specifies a
+timer interval (in µs) within which to count authentication attempts. When the
+counter goes above the value configured n `rateLimitIntervalBurst` log-ins are
+temporarily refused until the interval passes.
+
+`rateLimitIntervalBurst` → An unsigned 64bit integer, closely related to
+`rateLimitIntervalUSec`, that puts a limit on authentication attempts within
+the configured time interval.
+
+`enforcePasswordPolicy` → A boolean. Configures whether to enforce the system's
+password policy when creating the home directory for the user or changing the
+user's password. By default the policy is enforced, but if this field is false
+it is bypassed.
+
+`autoLogin` → A boolean. If true the user record is marked as suitable for
+auto-login. Systems are supposed to automatically log in a user marked this way
+during boot, if there's exactly one user on it defined this way.
+
+`stopDelayUSec` → An unsigned 64bit integer, indicating the time in µs the
+per-user service manager is kept around after the user fully logged out. This
+value is honored by
+[`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html). If
+set to zero the per-user service manager is immediately terminated when the
+user logs out, and longer values optimize high-frequency log-ins as the
+necessary work to set up and tear down a log-in is reduced if the service
+manager stays running.
+
+`killProcesses` → A boolean. If true all processes of the user are
+automatically killed when the user logs out. This is enforced by
+[`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html). If
+false any processes left around when the user logs out are left running.
+
+`passwordChangeMinUSec`/`passwordChangeMaxUSec` → An unsigned 64bit integer,
+encoding how much time has to pass at least/at most between password changes of
+the user. This corresponds with the `sp_min` and `sp_max` fields of `struct
+spwd` (i.e. the `/etc/shadow` entries of the user), but offers finer
+granularity.
+
+`passwordChangeWarnUSec` → An unsigned 64bit integer, encoding how much time to
+warn the user before their password expires, in µs. This corresponds with the
+`sp_warn` field of `struct spwd`.
+
+`passwordChangeInactiveUSec` → An unsigned 64bit integer, encoding how much
+time has to pass after the password expired that the account is
+deactivated. This corresponds with the `sp_inact` field of `struct spwd`.
+
+`passwordChangeNow` → A boolean. If true the user has to change their password
+on next login. This corresponds with the `sp_lstchg` field of `struct spwd`
+being set to zero.
+
+`pkcs11TokenUri` → An array of strings, each with an RFC 7512 compliant PKCS#11
+URI referring to security token (or smart card) of some form, that shall be
+associated with the user and may be used for authentication. The URI is used to
+search for an X.509 certificate and associated private key that may be used to
+decrypt an encrypted secret key that is used to unlock the user's account (see
+below). It's undefined how precise the URI is: during log-in it is tested
+against all plugged in security tokens and if there's exactly one matching
+private key found with it it is used.
+
+`privileged` → An object, which contains the fields of he `privileged` section
+of the user record, see below.
+
+`perMachine` → An array of objects, which contain the `perMachine` section of
+the user record, and thus fields to apply on specific systems only, see below.
+
+`binding` → An object, keyed by machine IDs formatted as strings, pointing
+to objects that contain the `binding` section of the user record,
+i.e. additional fields that bind the user record to a specific machine, see
+below.
+
+`status` → An object, keyed by machine IDs formatted as strings, pointing to
+objects that contain the `status` section of the user record, i.e. additional
+runtime fields that expose the current status of the user record on a specific
+system, see below.
+
+`signature` → An array of objects, which contain cryptographic signatures of
+the user record, i.e. the fields of the `signature` section of the user record,
+see below.
+
+`secret` → An object, which contains the fields of the `secret` section of the
+user record, see below.
+
+## Fields in the `privileged` section
+
+As mentioned, the `privileged` section is encoded in a sub-object of the user
+record top-level object, in the `privileged` field. Any data included in this
+object shall only be visible to the administrator and the user themselves, and
+be suppressed implicitly when other users get access to a user record. It thus
+takes the role of the `/etc/shadow` records for each user, which has similarly
+restrictive access semantics. The following fields are currently defined:
+
+`passwordHint` → A user-selected password hint in free-form text. This should
+be a string like "What's the name of your first pet?", but is entirely for the
+user to choose.
+
+`hashPassword` → An array of strings, each containing a hashed UNIX password
+string, in the format
+[`crypt(3)`](http://man7.org/linux/man-pages/man3/crypt.3.html) generates. This
+corresponds with `sp_pwdp` field of `struct spwd` (and in a way the `pw_passwd`
+field of `struct passwd`).
+
+`sshAuthorizedKeys` → An array of strings, each listing an SSH public key that
+is authorized to access the account. The strings should follow the same format
+as the lines in the traditional `~/.ssh/authorized_key` file.
+
+`pkcs11EncryptedKey` → An array of objects. Each element of the array should be
+an object consisting of three string fields: `uri` shall contain a PKCS#11
+security token URI, `data` shall contain a Base64 encoded encrypted key and
+`hashedPassword` shall contain a UNIX password hash to test the key
+against. Authenticating with a security token against this account shall work
+as follows: the encrypted secret key is converted from its Base64
+representation into binary, then decrypted with the PKCS#11 `C_Decrypt()`
+function of the PKCS#11 module referenced by the specified URI, using the
+private key found on the same token. The resulting decrypted key is then
+Base64-encoded and tested against the specified UNIX hashed password. The
+Base64-enceded decrypted key may also be used to unlock further resources
+during log-in, for example the LUKS or `fscrypt` storage backend. It is
+generally recommended that for each entry in `pkcs11EncryptedKey` there's also
+a matching one in `pkcs11TokenUri` and vice versa, with the same URI, appearing
+in the same order, but this should not be required by applications processing
+user records.
+
+## Fields in the `perMachine` section
+
+As mentioned, the `perMachine` section contains settings that shall apply to
+specific systems only. This is primarily interesting for resource management
+properties as they tend to require a per-system focus, however they may be used
+for other purposes too.
+
+The `perMachine` field in the top-level object is an array of objects. When
+processing the user record first the various fields on the top-level object
+should be used. Then this array should be iterated in order, and the various
+settings be applied that match either the indicated machine ID or host
+name. There may be multiple array entries that match a specific system, in
+which case all the object's setting should be applied. If the same option is
+set in the top-level object as in a per-machine object the latter wins and
+entirely undoes the setting in the top-level object (i.e. no merging of
+properties that are arrays themselves is done). If the same option is set in
+multiple per-machine objects the one specified later in the array wins (and
+here too no merging of individual fields is done, the later field always wins
+in full).
+
+The following fields are defined in this section:
+
+`matchMachineId` → An array of strings with each a formatted 128bit ID in
+hex. If any of the specified IDs match the system's local machine ID
+(i.e. matches `/etc/machine-id`) the fields in this object are honored.
+
+`matchHostname` → An array of string with a each a valid hostname. If any of
+the specified hostnames match the system's local hostname, the fields in this
+object are honored. If both `matchHostname` and `matchMachineId` are used
+within the same array entry, the object is honored when either match succeeds,
+i.e. the two match types are combined in OR, not in AND.
+
+These two are the only two fields specific to this section. All other fields
+that may be used in this section are identical to the equally named ones in the
+`regular` section (i.e. at the top-level object). Specifically, these are:
+
+`iconName`, `location`, `shell`, `umask`, `environment`, `timeZone`,
+`preferredLanguage`, `niceLevel`, `resourceLimits`, `locked`, `notBeforeUSec`,
+`notAfterUSec`, `storage`, `diskSize`, `diskSizeRelative`, `skeletonDirectory`,
+`accessMode`, `tasksMax`, `memoryHigh`, `memoryMax`, `cpuWeight`, `ioWeight`,
+`mountNoDevices`, `mountNoSuid`, `mountNoExecute`, `cifsDomain`,
+`cifsUserName`, `cifsService`, `imagePath`, `uid`, `gid`, `memberOf`,
+`fileSystemType`, `partitionUuid`, `luksUuid`, `fileSystemUuid`, `luksDiscard`,
+`luksCipher`, `luksCipherMode`, `luksVolumeKeySize`, `luksPbkdfHashAlgorithm`,
+`luksPbkdfType`, `luksPbkdfTimeCostUSec`, `luksPbkdfMemoryCost`,
+`luksPbkdfParallelThreads`, `rateLimitIntervalUSec`, `rateLimitBurst`,
+`enforcePasswordPolicy`, `autoLogin`, `stopDelayUSec`, `killProcesses`,
+`passwordChangeMinUSec`, `passwordChangeMaxUSec`, `passwordChangeWarnUSec`,
+`passwordChangeInactiveUSec`, `passwordChangeNow`, `pkcs11TokenUri`.
+
+## Fields in the `binding` section
+
+As mentioned, the `binding` section contains additional fields about the user
+record, that bind it to the local system. These fields are generally used by a
+local user manager (such as `systemd-homed.service`) to add in fields that make
+sense in a local context but not necessarily in a global one. For example, a
+user record that contains no `uid` field in the regular section is likely
+extended with one in the `binding` section to assign a local UID if no global
+UID is defined.
+
+All fields in the `binding` section only make sense in a local context and are
+suppressed when the user record is ported between systems. The `binding` section
+is generally persisted on the system but not in the home directories themselves
+and the home directory is supposed to be fully portable and thus not contain
+the information that `binding` is supposed to contain that binds the portable
+record to a specific system.
+
+The `binding` sub-object on the top-level user record object is keyed by the
+machine ID the binding is intended for, which point to an object with the
+fields of the bindings. These fields generally match fields that may also be
+defined in the `regular` and `perMachine` sections, however override
+both. Usually, the `binding` value should not contain settings different from
+those set via `regular` or `perMachine`, however this might happen if some
+settings are not supported locally (think: `fscrypt` is recorded as intended
+storage mechanism in the `regular` section, but the local kernel does not
+support `fscrypt`, hence `directory` was chosen as implicit fallback), or have
+been changed in the `regular` section through updates (e.g. a home directory
+was created with `luks` as storage mechanism but later the user record was
+updated to prefer `subvolume`, which however doesn't change the actual storage
+used already which is pinned in the `binding` section).
+
+The following fields are defined in the `binding` section. They all have an
+identical format and override their equally named counterparts in the `regular`
+and `perMachine` sections:
+
+`imagePath`, `homeDirectory`, `partitionUuid`, `luksUuid`, `fileSystemUuid`,
+`uid`, `gid`, `storage`, `fileSystemType`, `luksCipher`, `luksCipherMode`,
+`luksVolumeKeySize`.
+
+## Fields in the `status` section
+
+As mentioned, the `status` section contains additional fields about the user
+record that are exclusively acquired during runtime, and that expose runtime
+metrics of the user and similar metadata that shall not be persisted but are
+only acquired "on-the-fly" when requested.
+
+This section is arranged similarly to the `binding` section: the `status`
+sub-object of the top-level user record object is keyed by the machine ID,
+which points to the object with the fields defined here. The following fields
+are defined:
+
+`diskUsage` → An unsigned 64bit integer. The currently used disk space of the
+home directory in bytes. This value might be determined in different ways,
+depending on the selected storage mechanism. For LUKS storage this is the file
+size of the loopback file or block device size. For the
+directory/subvolume/fscrypt storage this is the current disk space used as
+reported by the file system quota subsystem.
+
+`diskFree` → An unsigned 64bit integer, denoting the number of "free" bytes in
+the disk space allotment, i.e. usually the difference between the disk size as
+reported by `diskSize` and the used already as reported in `diskFree`, but
+possibly skewed by metadata sizes, disk compression and similar.
+
+`diskSize` → An unsigned 64bit integer, denoting the disk space currently
+allotted to the user, in bytes. Depending on the storage mechanism this can mean
+different things (see above). In contrast to the top-level field of the same
+(or the one in the `perMachine` section), this field reports the current size
+allotted to the user, not the intended one. The values may differ when user
+records are updated without the home directory being re-sized.
+
+`diskCeiling`/`diskFloor` → Unsigned 64bit integers indicating upper and lower
+bounds when changing the `diskSize` value, in bytes. These values are typically
+derived from the underlying data storage, and indicate in which range the home
+directory may be re-sized in, i.e. in which sensible range the `diskSize` value
+should be kept.
+
+`state` → A string indicating the current state of the home directory. The
+precise set of values exposed here are up to the service managing the home
+directory to define (i.e. are up to the service identified with the `service`
+field below). However, it is recommended to stick to a basic vocabulary here:
+`inactive` for a home directory currently not mounted, `absent` for a home
+directory that cannot be mounted currently because it does not exist on the
+local system, `active` for a home directory that is currently mounted and
+accessible.
+
+`service` → A string identifying the service that manages this user record. For
+example `systemd-homed.service` sets this to `io.systemd.Home` to all user
+records it manages. This is particularly relevant to define clearly the context
+in which `state` lives, see above. Note that this field also exists on the
+top-level object (i.e. in the `regular` section), which it overrides. The
+`regular` field should be used if conceptually the user record can only be
+managed by the specified service, and this `status` field if it can
+conceptually be managed by different managers, but currently is managed by the
+specified one.
+
+`signedLocally` → A boolean. If true indicates that the user record is signed
+by a public key for which the private key is available locally. This means that
+the user record may be modified locally as it can be re-signed with the private
+key. If false indicates that the user record is signed by a public key
+recognized by the local manager but whose private key is not available
+locally. This means the user record cannot be modified locally as it couldn't
+be signed afterwards.
+
+`goodAuthenticationCounter` → An unsigned 64bit integer. This counter is
+increased by one on every successful authentication attempt, i.e. an
+authentication attempt where a security token of some form was presented and it
+was correct.
+
+`badAuthenticationCounter` → An unsigned 64bit integer. This counter is
+increased by one on every unsuccessfully authentication attempt, i.e. an
+authentication attempt where a security token of some form was presented and it
+was incorrect.
+
+`lastGoodAuthenticationUSec` → An unsigned 64bit integer, indicating the time
+of the last successful authentication attempt in µs since the UNIX epoch (1970).
+
+`lastBadAuthenticationUSec` → Similar, but the timestamp of the last
+unsuccessfully authentication attempt.
+
+`rateLimitBeginUSec` → An unsigned 64bit integer: the µs timestamp since the
+UNIX epoch (1970) where the most recent rate limiting interval has been
+started, as configured with `rateLimitIntervalUSec`.
+
+`rateLimitCount` → An unsigned 64bit integer, counting the authentication
+attempts in the current rate limiting interval, see above. If this counter
+grows beyond the value configured in `rateLimitBurst` authentication attempts
+are temporarily refused.
+
+`removable` → A boolean value. If true the manager of this user record
+determined the home directory being on removable media. If false it was
+determined the home directory is in internal built-in media. (This is used by
+`systemd-logind.service` to automatically pick the right default value for
+`stopDelayUSec` if the field is not explicitly specified: for home directories
+on removable media the delay is selected very low to minimize the chance the
+home directory remains in unclean state if the storage device is removed from
+the system by the user).
+
+## Fields in the `signature` section
+
+As mentioned, the `signature` section of the user record may contain one or
+more cryptographic signatures of the user record. Like all others, this section
+is optional, and only used when cryptographic validation of user records shall
+be used. Specifically, all user records managed by `systemd-homed.service` will
+carry such signatures and the service refuses managing user records that come
+without signature or with signatures not recognized by any locally defined
+public key.
+
+The `signature` field in the top-level user record object is an array of
+objects. Each object encapsulates one signature and has two fields: `data` and
+`key` (both are strings). The `data` field contains the actual signature,
+encoded in base64, the `key` field contains a copy of the public key whose
+private key was used to make the signature, in PEM format. Currently only
+signatures with Ed25519 keys are defined.
+
+Before signing the user record should be brought into "normalized" form,
+i.e. the keys in all objects should be sorted alphabetically. All redundant
+white-space and newlines should be removed and the JSON text then signed.
+
+The signatures only cover the `regular`, `perMachine` and `privileged` sections
+of the user records, all other sections (include `signature` itself), are
+removed before the signature is calculated.
+
+Rationale for signing and threat model: while a multi-user operating system
+like Linux strives for being sufficiently secure even after a user acquired a
+local login session reality tells us this is not the case. Hence it is
+essential to restrict carefully which users may gain access to a system and
+which ones shall not. A minimal level of trust must be established between
+system, user record and the user themselves before a log-in request may be
+permitted. In particular if the home directory is provided in its own LUKS2
+encapsulated file system it is essential this trust is established before the
+user logs in (and hence the file system mounted), since file system
+implementations on Linux are well known to be relatively vulnerable to rogue
+disk images. User records and home directories in many context are expected to
+be something shareable between multiple systems, and the transfer between them
+might not happen via exclusively trusted channels. Hence it's essential that
+the user record is not manipulated between uses. Finally, resource management
+(which may be done by the various fields of the user record) is security
+sensitive, since it should forcefully lock the user into the assigned resource
+usage and not allow them to use more. The requirement of being able to trust
+the user record data combined with the potential transfer over untrusted
+channels suggest a cryptographic signature mechanism where only user records
+signed by a recognized key are permitted to log in locally.
+
+Note that other mechanisms for establishing sufficient trust exist too, and are
+perfectly valid as well. For example, systems like LDAP/ActiveDirectory
+generally insist on user record transfer from trusted servers via encrypted TLS
+channels only. Or traditional UNIX users created locally in `/etc/passwd` never
+exist outside of the local trusted system, hence transfer and trust in the
+source are not an issue. The major benefit of operating with signed user
+records is that they are self-sufficiently trusted, not relying on a secure
+channel for transfer, and thus being compatible with a more distributed model
+of home directory transfer, including on USB sticks and such.
+
+## Fields in the `secret` section
+
+As mentioned, the `secret` section of the user record should never be persisted
+nor transferred across machines. It is only defined in short-lived operations,
+for example when a user record is first created or registered, as the secret
+key data needs to be available to derive encryption keys from and similar.
+
+The `secret` field of the top-level user record contains the following fields:
+
+`password` → an array of strings, each containing a plain text password.
+
+`pkcs11Pin` → an array of strings, each containing a plain text PIN, suitable
+for unlocking PKCS#11 security tokens that require that.
+
+`pkcs11ProtectedAuthenticationPathPermitted` → a boolean. If set to true allows
+the receiver to use the PKCS#11 "protected authentication path" (i.e. a
+physical button/touch element on the security token) for authenticating the
+user. If false or unset authentication this way shall not be attempted.
+
+## Mapping to `struct passwd` and `struct spwd`
+
+When mapping classic UNIX user records (i.e. `struct passwd` and `struct spwd`)
+to JSON user records the following mappings should be applied:
+
+| Structure | Field | Section | Field | Condition |
+|-----------------|-------------|--------------|------------------------------|----------------------------|
+| `struct passwd` | `pw_name` | `regular` | `userName` | |
+| `struct passwd` | `pw_passwd` | `privileged` | `password` | (See notes below) |
+| `struct passwd` | `pw_uid` | `regular` | `uid` | |
+| `struct passwd` | `pw_gid` | `regular` | `gid` | |
+| `struct passwd` | `pw_gecos` | `regular` | `realName` | |
+| `struct passwd` | `pw_dir` | `regular` | `homeDirectory` | |
+| `struct passwd` | `pw_shell` | `regular` | `shell` | |
+| `struct spwd` | `sp_namp` | `regular` | `userName` | |
+| `struct spwd` | `sp_pwdp` | `privileged` | `password` | (See notes below) |
+| `struct spwd` | `sp_lstchg` | `regular` | `lastPasswordChangeUSec` | (if `sp_lstchg` > 0) |
+| `struct spwd` | `sp_lstchg` | `regular` | `passwordChangeNow` | (if `sp_lstchg` == 0) |
+| `struct spwd` | `sp_min` | `regular` | `passwordChangeMinUSec` | |
+| `struct spwd` | `sp_max` | `regular` | `passwordChangeMaxUSec` | |
+| `struct spwd` | `sp_warn` | `regular` | `passwordChangeWarnUSec` | |
+| `struct spwd` | `sp_inact` | `regular` | `passwordChangeInactiveUSec` | |
+| `struct spwd` | `sp_expire` | `regular` | `locked` | (if `sp_expire` in [0, 1]) |
+| `struct spwd` | `sp_expire` | `regular` | `notAfterUSec` | (if `sp_expire` > 1) |
+
+At this time almost all Linux machines employ shadow passwords, thus the
+`pw_passwd` field in `struct passwd` is set to `"x"`, and the actual password
+is stored in the shadow entry `struct spwd`'s field `sp_pwdp`.
+
+## Extending These Records
+
+User records following this specifications are supposed to be extendable for
+various applications. In general, subsystems are free to introduce their own
+keys, as long as:
+
+* Care should be taken to place the keys in the right section, i.e. the most
+ appropriate for the data field.
+
+* Care should be taken to avoid namespace clashes. Please prefix your fields
+ with a short identifier of your project to avoid ambiguities and
+ incompatibilities.
+
+* This specification is supposed to be a living specification. If you need
+ additional fields, please consider submitting them upstream for inclusion in
+ this specification. If they are reasonably universally useful, it would be
+ best to list them here.
+
+## Examples
+
+The shortest valid user record looks like this:
+
+```json
+{
+ "userName" : "u"
+}
+```
+
+A reasonable user record for a system user might look like this:
+
+```json
+{
+ "userName" : "httpd",
+ "uid" : 473,
+ "gid" : 473,
+ "disposition" : "system",
+ "locked" : true
+}
+```
+
+A fully featured user record associated with a home directory managed by
+`systemd-homed.service` might look like this:
+
+```json
+{
+ "autoLogin" : true,
+ "binding" : {
+ "15e19cf24e004b949ddaac60c74aa165" : {
+ "fileSystemType" : "ext4",
+ "fileSystemUuid" : "758e88c8-5851-4a2a-b88f-e7474279c111",
+ "gid" : 60232,
+ "homeDirectory" : "/home/grobie",
+ "imagePath" : "/home/grobie.home",
+ "luksCipher" : "aes",
+ "luksCipherMode" : "xts-plain64",
+ "luksUuid" : "e63581ba-79fb-4226-b9de-1888393f7573",
+ "luksVolumeKeySize" : 32,
+ "partitionUuid" : "41f9ce04-c827-4b74-a981-c669f93eb4dc",
+ "storage" : "luks",
+ "uid" : 60232
+ }
+ },
+ "disposition" : "regular",
+ "enforcePasswordPolicy" : false,
+ "lastChangeUSec" : 1565950024279735,
+ "memberOf" : [
+ "wheel"
+ ],
+ "privileged" : {
+ "hashedPassword" : [
+ "$6$WHBKvAFFT9jKPA4k$OPY4D4TczKN/jOnJzy54DDuOOagCcvxxybrwMbe1SVdm.Bbr.zOmBdATp.QrwZmvqyr8/SafbbQu.QZ2rRvDs/"
+ ]
+ },
+ "signature" : [
+ {
+ "data" : "LU/HeVrPZSzi3MJ0PVHwD5m/xf51XDYCrSpbDRNBdtF4fDVhrN0t2I2OqH/1yXiBidXlV0ptMuQVq8KVICdEDw==",
+ "key" : "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA/QT6kQWOAMhDJf56jBmszEQQpJHqDsGDMZOdiptBgRk=\n-----END PUBLIC KEY-----\n"
+ }
+ ],
+ "userName" : "grobie",
+ "status" : {
+ "15e19cf24e004b949ddaac60c74aa165" : {
+ "goodAuthenticationCounter" : 16,
+ "lastGoodAuthenticationUSec" : 1566309343044322,
+ "rateLimitBeginUSec" : 1566309342340723,
+ "rateLimitCount" : 1,
+ "state" : "inactive",
+ "service" : "io.systemd.Home",
+ "diskSize" : 161118667776,
+ "diskCeiling" : 190371729408,
+ "diskFloor" : 5242880,
+ "signedLocally" : true
+ }
+ }
+}
+```
+
+When `systemd-homed.service` manages a home directory it will also include a
+version of the user record in the home directory itself in the `~/.identity`
+file. This version lacks the `binding` and `status` sections which are used for
+local management of the user, but are not intended to be portable between
+systems. It would hence look like this:
+
+```json
+{
+ "autoLogin" : true,
+ "disposition" : "regular",
+ "enforcePasswordPolicy" : false,
+ "lastChangeUSec" : 1565950024279735,
+ "memberOf" : [
+ "wheel"
+ ],
+ "privileged" : {
+ "hashedPassword" : [
+ "$6$WHBKvAFFT9jKPA4k$OPY4D4TczKN/jOnJzy54DDuOOagCcvxxybrwMbe1SVdm.Bbr.zOmBdATp.QrwZmvqyr8/SafbbQu.QZ2rRvDs/"
+ ]
+ },
+ "signature" : [
+ {
+ "data" : "LU/HeVrPZSzi3MJ0PVHwD5m/xf51XDYCrSpbDRNBdtF4fDVhrN0t2I2OqH/1yXiBidXlV0ptMuQVq8KVICdEDw==",
+ "key" : "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA/QT6kQWOAMhDJf56jBmszEQQpJHqDsGDMZOdiptBgRk=\n-----END PUBLIC KEY-----\n"
+ }
+ ],
+ "userName" : "grobie",
+}
+```
diff --git a/factory/etc/nsswitch.conf b/factory/etc/nsswitch.conf
index 5470993e34..e7365cd142 100644
--- a/factory/etc/nsswitch.conf
+++ b/factory/etc/nsswitch.conf
@@ -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
diff --git a/man/nss-systemd.xml b/man/nss-systemd.xml
index 8fde11867c..e343c406f2 100644
--- a/man/nss-systemd.xml
+++ b/man/nss-systemd.xml
@@ -18,7 +18,7 @@
nss-systemdlibnss_systemd.so.2
- Provide UNIX user and group name resolution for dynamic users and groups.
+ Provide UNIX user and group name resolution for user/group lookup via Varlink
@@ -28,16 +28,24 @@
Description
- nss-systemd is a plug-in module for the GNU Name Service Switch (NSS) functionality of the
- GNU C Library (glibc), providing UNIX user and group name resolution for dynamic users and
- groups allocated through the DynamicUser= option in systemd unit files. See
- systemd.exec5 for details on
- this option.
+ nss-systemd is a plug-in module for the GNU Name Service Switch (NSS)
+ functionality of the GNU C Library (glibc), providing UNIX user and group name
+ resolution for services implementing the User/Group Record
+ Lookup API via Varlink, such as the system and service manager
+ systemd1 (for its
+ DynamicUser= feature, see
+ systemd.exec5 for
+ details) or
+ systemd-homed.service8.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 /etc/passwd or
/etc/group, or if these files are missing.
+ This module preferably utilizes
+ systemd-userdbd.service8
+ for resolving users and groups, but also works without the service running.
+
To activate the NSS module, add systemd to the lines starting with
passwd: and group: in /etc/nsswitch.conf.
@@ -54,7 +62,7 @@
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
diff --git a/man/pam_systemd.xml b/man/pam_systemd.xml
index 470c1a4c42..694371c2c4 100644
--- a/man/pam_systemd.xml
+++ b/man/pam_systemd.xml
@@ -32,6 +32,10 @@
systemd-logind.service8,
and hence the systemd control group hierarchy.
+ The module also applies various resource management and runtime parameters to the new session, as
+ configured in the JSON User Record of the user, when
+ one is defined.
+
On login, this module — in conjunction with systemd-logind.service — ensures the
following:
@@ -48,7 +52,12 @@
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 user.slice is automatically created and the
scope placed into it. An instance of the system service user@.service, which runs the
- systemd user manager instance, is started.
+ systemd user manager instance, is started.
+
+ The $TZ, $EMAIL and $LANG
+ 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.On logout, this module ensures the following:
@@ -172,6 +181,15 @@
is not set if the current user is not the original user of the session.
+
+ $TZ
+ $EMAIL
+ $LANG
+
+ If a JSON user record is known for the user logging in these variables are
+ initialized from the respective data in the record.
+
+
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);
Example
+ Here's an example PAM configuration fragment that allows users sessions to be managed by
+ systemd-logind.service:
+
#%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
+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
@@ -303,6 +330,7 @@ session required pam_systemd.so
systemd-logind.service8,
logind.conf5,
loginctl1,
+ pam_systemd_home8,
pam.conf5,
pam.d5,
pam8,
diff --git a/man/rules/meson.build b/man/rules/meson.build
index f33621a654..90376da775 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -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.
diff --git a/man/systemd-userdbd.service.xml b/man/systemd-userdbd.service.xml
new file mode 100644
index 0000000000..9d5841c0f0
--- /dev/null
+++ b/man/systemd-userdbd.service.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+ systemd-userdbd.service
+ systemd
+
+
+
+ systemd-userdbd.service
+ 8
+
+
+
+ systemd-userdbd.service
+ systemd-userdbd
+ JSON User/Group Record Query Multiplexer/NSS Compatibility
+
+
+
+ systemd-userdbd.service
+ /usr/lib/systemd/systemd-userdbd
+
+
+
+ Description
+
+ systemd-userdbd 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.
+
+ Most of systemd-userdbd's functionality is accessible through the
+ userdbctl1
+ command.
+
+ The user and group records this service provides access to follow the JSON User Record and JSON Group Record definitions. This service implements the
+ User/Group Record Lookup API via Varlink, and
+ multiplexes access other services implementing this API, too. It is thus both server and client of this
+ API.
+
+ This service provides two distinct Varlink services:
+ io.systemd.Multiplexer 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. io.systemd.NameSeviceSwitch provides compatibility with classic UNIX/glibc
+ NSS user records, i.e. converts struct passwd and struct group records as
+ acquired with APIs such as getpwnam1 to JSON
+ user/group records, thus hiding the differences between the services as much as possible.
+
+
+
+ See Also
+
+ systemd1,
+ nss-systemd8,
+ userdbctl1,
+ systemd-homed.service8
+
+
+
diff --git a/man/userdbctl.xml b/man/userdbctl.xml
new file mode 100644
index 0000000000..69cd3fc067
--- /dev/null
+++ b/man/userdbctl.xml
@@ -0,0 +1,258 @@
+
+
+
+
+
+
+
+ userdbctl
+ systemd
+
+
+
+ userdbctl
+ 1
+
+
+
+ userdbctl
+ Inspect users, groups and group memberships
+
+
+
+
+ userdbctl
+ OPTIONS
+ COMMAND
+ NAME
+
+
+
+
+ Description
+
+ userdbctl 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 JSON User Record and JSON Group Record definitions), and classic UNIX NSS/glibc
+ user and group records. This tool is primarily a client to the User/Group Record Lookup API via Varlink.
+
+
+
+ Options
+
+ The following options are understood:
+
+
+
+
+ MODE
+
+ Choose the output mode, takes one of classic,
+ friendly, table, json. If
+ classic, an output very close to the format of /etc/passwd or
+ /etc/group is generated. If friendly a more comprehensive and
+ user friendly, human readable output is generated; if table a minimal, tabular
+ output is generated; if json a JSON formatted output is generated. Defaults to
+ friendly if a user/group is specified on the command line,
+ table otherwise.
+
+
+
+ SERVICE:SERVICE…
+ SERVICE:SERVICE…
+
+ Controls which services to query for users/groups. Takes a list of one or more
+ service names, separated by :. See below for a list of well-known service
+ names. If not specified all available services are queried at once.
+
+
+
+ BOOL
+
+ Controls whether to include classic glibc/NSS user/group lookups in the output. If
+ is used any attempts to resolve or enumerate users/groups provided
+ only via glibc NSS is suppressed. If is specified such users/groups
+ are included in the output (which is the default).
+
+
+
+ BOOL
+
+ Controls whether to synthesize records for the root and nobody users/groups if they
+ aren't defined otherwise. By default (or yes) such records are implicitly
+ synthesized if otherwise missing since they have special significance to the OS. When
+ no this synthesizing is turned off.
+
+
+
+
+
+ This option is short for
+ . 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.
+
+
+
+
+
+
+
+
+
+
+ Commands
+
+ The following commands are understood:
+
+
+
+
+ userUSER…
+
+ List all known users records or show details of one or more specified user
+ records. Use to tweak output mode.
+
+
+
+ groupGROUP…
+
+ List all known group records or show details of one or more specified group
+ records. Use to tweak output mode.
+
+
+
+ users-in-groupGROUP…
+
+ List users that are members of the specified groups. If no groups are specified list
+ all user/group memberships defined. Use to tweak output
+ mode.
+
+
+
+ groups-of-userUSER…
+
+ List groups that the specified users are members of. If no users are specified list
+ all user/group memberships defined (in this case groups-of-user and
+ users-in-group are equivalent). Use to tweak output
+ mode.
+
+
+
+ services
+
+ List all services currently providing user/group definitions to the system. See below
+ for a list of well-known services providing user information.
+
+
+
+ ssh-authorized-keys
+
+ 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.
+
+
+
+
+
+ Well-Known Services
+
+ The userdbctl services 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.
+
+
+
+
+ io.systemd.DynamicUser
+
+ This service is provided by the system service manager itself (i.e. PID 1) and
+ makes all users (and their groups) synthesized through the DynamicUser= setting in
+ service unit files available to the system (see
+ systemd.exec5 for
+ details about this setting).
+
+
+
+ io.systemd.Home
+
+ This service is provided by
+ systemd-homed.service8
+ and makes all users (and their groups) belonging to home directories managed by that service
+ available to the system.
+
+
+
+ io.systemd.Multiplexer
+
+ This service is provided by
+ systemd-userdbd.service8
+ 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.
+ userdbctl uses this service preferably, too, unless
+ or are used, in which case finer control over the services to talk to is
+ required.
+
+
+
+ io.systemd.NameSeviceSwitch
+
+ This service is (also) provided by
+ systemd-userdbd.service8
+ and converts classic NSS/glibc user and group records to JSON user/group records, providing full
+ backwards compatibility. Use to disable this compatibility, see
+ above. Note that compatibility is actually provided in both directions:
+ nss-systemd8 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.
+
+
+
+ Note that userdbctl has internal support for NSS-based lookups too. This means
+ that if neither io.systemd.Multiplexer nor
+ io.systemd.NameSeviceSwitch are running look-ups into the the basic user/group
+ databases will still work.
+
+
+
+ Integration with SSH
+
+ The userdbctl 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 sshd_config5:
+
+ …
+AuthorizedKeysCommand /usr/bin/userdbctl ssh-authorized-keys %u
+AuthorizedKeysCommandUser root
+…
+
+
+
+ Exit status
+
+ On success, 0 is returned, a non-zero failure code otherwise.
+
+
+
+
+
+ See Also
+
+ systemd1,
+ systemd-userdbd.service8,
+ systemd-homed.service8,
+ nss-systemd8,
+ getent1
+
+
+
+
diff --git a/meson.build b/meson.build
index 3b3786bbdb..46f3e9c7ba 100644
--- a/meson.build
+++ b/meson.build
@@ -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'],
diff --git a/meson_options.txt b/meson_options.txt
index 4d641c3671..6736240f39 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -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',
diff --git a/src/basic/nss-util.h b/src/basic/nss-util.h
index 2045175d1c..29cf22676a 100644
--- a/src/basic/nss-util.h
+++ b/src/basic/nss-util.h
@@ -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,
diff --git a/src/basic/user-util.c b/src/basic/user-util.c
index 571578c4e4..68c6dd0da7 100644
--- a/src/basic/user-util.c
+++ b/src/basic/user-util.c
@@ -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;
-}
diff --git a/src/basic/user-util.h b/src/basic/user-util.h
index e11dd9b379..714e83082c 100644
--- a/src/basic/user-util.h
+++ b/src/basic/user-util.h
@@ -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);
diff --git a/src/core/core-varlink.c b/src/core/core-varlink.c
new file mode 100644
index 0000000000..628b9ccaaa
--- /dev/null
+++ b/src/core/core-varlink.c
@@ -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);
+}
diff --git a/src/core/core-varlink.h b/src/core/core-varlink.h
new file mode 100644
index 0000000000..89818e2766
--- /dev/null
+++ b/src/core/core-varlink.h
@@ -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);
diff --git a/src/core/dynamic-user.c b/src/core/dynamic-user.c
index 75373407b4..e00109ee4a 100644
--- a/src/core/dynamic-user.c
+++ b/src/core/dynamic-user.c
@@ -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 */
diff --git a/src/core/manager.c b/src/core/manager.c
index dbd25af27e..cb0633e2ab 100644
--- a/src/core/manager.c
+++ b/src/core/manager.c
@@ -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);
diff --git a/src/core/manager.h b/src/core/manager.h
index 8ca8e3885f..67f9af5fc6 100644
--- a/src/core/manager.h
+++ b/src/core/manager.h
@@ -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) {
diff --git a/src/core/meson.build b/src/core/meson.build
index e0f08c057e..3586838f59 100644
--- a/src/core/meson.build
+++ b/src/core/meson.build
@@ -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
diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c
index 528e6452cf..8001f90d57 100644
--- a/src/firstboot/firstboot.c
+++ b/src/firstboot/firstboot.c
@@ -4,19 +4,6 @@
#include
#include
-#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
-#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"
diff --git a/src/login/logind-core.c b/src/login/logind-core.c
index e84b38096a..22a42b077c 100644
--- a/src/login/logind-core.c
+++ b/src/login/logind-core.c
@@ -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) {
diff --git a/src/login/logind-dbus.c b/src/login/logind-dbus.c
index 69b5994878..d3aa6815a6 100644
--- a/src/login/logind-dbus.c
+++ b/src/login/logind-dbus.c
@@ -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;
diff --git a/src/login/logind-seat.c b/src/login/logind-seat.c
index 026bcf4d8e..394f89dd7e 100644
--- a/src/login/logind-seat.c
+++ b/src/login/logind-seat.c
@@ -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");
diff --git a/src/login/logind-session-dbus.c b/src/login/logind-session-dbus.c
index 32a64fff0c..80f3e432de 100644
--- a/src/login/logind-session-dbus.c
+++ b/src/login/logind-session-dbus.c
@@ -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);
diff --git a/src/login/logind-session.c b/src/login/logind-session.c
index a9be416f52..48505122a9 100644
--- a/src/login/logind-session.c
+++ b/src/login/logind-session.c
@@ -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",
diff --git a/src/login/logind-user-dbus.c b/src/login/logind-user-dbus.c
index 1dc222f90e..7943a007e1 100644
--- a/src/login/logind-user-dbus.c
+++ b/src/login/logind-user-dbus.c
@@ -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, ...) {
diff --git a/src/login/logind-user.c b/src/login/logind-user.c
index 1550431bea..fdc6448a65 100644
--- a/src/login/logind-user.c
+++ b/src/login/logind-user.c
@@ -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));
}
}
diff --git a/src/login/logind-user.h b/src/login/logind-user.h
index 4bd65d8373..f8f172cb0f 100644
--- a/src/login/logind-user.h
+++ b/src/login/logind-user.h
@@ -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);
diff --git a/src/login/logind.h b/src/login/logind.h
index f0f8928ef2..7e4673d27d 100644
--- a/src/login/logind.h
+++ b/src/login/logind.h
@@ -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);
diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c
index aa6e5ea7ac..c7fe858b92 100644
--- a/src/login/pam_systemd.c
+++ b/src/login/pam_systemd.c
@@ -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;
}
diff --git a/src/nss-systemd/nss-systemd.c b/src/nss-systemd/nss-systemd.c
index 8ef1cd5ea9..34f886a8cc 100644
--- a/src/nss-systemd/nss-systemd.c
+++ b/src/nss-systemd/nss-systemd.c
@@ -3,28 +3,17 @@
#include
#include
-#include "sd-bus.h"
-
-#include "alloc-util.h"
-#include "bus-common-errors.h"
-#include "dirent-util.h"
#include "env-util.h"
+#include "errno-util.h"
#include "fd-util.h"
-#include "format-util.h"
-#include "fs-util.h"
-#include "list.h"
+#include "group-record-nss.h"
#include "macro.h"
#include "nss-util.h"
#include "signal-util.h"
-#include "stdio-util.h"
-#include "string-util.h"
+#include "strv.h"
#include "user-util.h"
-#include "util.h"
-
-#define DYNAMIC_USER_GECOS "Dynamic User"
-#define DYNAMIC_USER_PASSWD "*" /* locked */
-#define DYNAMIC_USER_DIR "/"
-#define DYNAMIC_USER_SHELL NOLOGIN
+#include "userdb-glue.h"
+#include "userdb.h"
static const struct passwd root_passwd = {
.pw_name = (char*) "root",
@@ -60,78 +49,32 @@ static const struct group nobody_group = {
.gr_mem = (char*[]) { NULL },
};
-typedef struct UserEntry UserEntry;
-typedef struct GetentData GetentData;
-
-struct UserEntry {
- uid_t id;
- char *name;
-
- GetentData *data;
- LIST_FIELDS(UserEntry, entries);
-};
-
-struct GetentData {
- /* As explained in NOTES section of getpwent_r(3) as 'getpwent_r() is not really
- * reentrant since it shares the reading position in the stream with all other threads',
- * we need to protect the data in UserEntry from multithreaded programs which may call
- * setpwent(), getpwent_r(), or endpwent() simultaneously. So, each function locks the
- * data by using the mutex below. */
+typedef struct GetentData {
+ /* As explained in NOTES section of getpwent_r(3) as 'getpwent_r() is not really reentrant since it
+ * shares the reading position in the stream with all other threads', we need to protect the data in
+ * UserDBIterator from multithreaded programs which may call setpwent(), getpwent_r(), or endpwent()
+ * simultaneously. So, each function locks the data by using the mutex below. */
pthread_mutex_t mutex;
+ UserDBIterator *iterator;
- UserEntry *position;
- LIST_HEAD(UserEntry, entries);
+ /* Applies to group iterations only: true while we iterate over groups defined through NSS, false
+ * otherwise. */
+ bool by_membership;
+} GetentData;
+
+static GetentData getpwent_data = {
+ .mutex = PTHREAD_MUTEX_INITIALIZER
};
-static GetentData getpwent_data = { PTHREAD_MUTEX_INITIALIZER, NULL, NULL };
-static GetentData getgrent_data = { PTHREAD_MUTEX_INITIALIZER, NULL, NULL };
+static GetentData getgrent_data = {
+ .mutex = PTHREAD_MUTEX_INITIALIZER
+};
NSS_GETPW_PROTOTYPES(systemd);
NSS_GETGR_PROTOTYPES(systemd);
-enum nss_status _nss_systemd_endpwent(void) _public_;
-enum nss_status _nss_systemd_setpwent(int stayopen) _public_;
-enum nss_status _nss_systemd_getpwent_r(struct passwd *result, char *buffer, size_t buflen, int *errnop) _public_;
-enum nss_status _nss_systemd_endgrent(void) _public_;
-enum nss_status _nss_systemd_setgrent(int stayopen) _public_;
-enum nss_status _nss_systemd_getgrent_r(struct group *result, char *buffer, size_t buflen, int *errnop) _public_;
-
-static int direct_lookup_name(const char *name, uid_t *ret) {
- _cleanup_free_ char *s = NULL;
- const char *path;
- int r;
-
- assert(name);
-
- /* Normally, we go via the bus to resolve names. That has the benefit that it is available from any mount
- * namespace and subject to proper authentication. However, there's one problem: if our module is called from
- * dbus-daemon itself we really can't use D-Bus to communicate. In this case, resort to a client-side hack,
- * and look for the dynamic names directly. This is pretty ugly, but breaks the cyclic dependency. */
-
- path = strjoina("/run/systemd/dynamic-uid/direct:", name);
- r = readlink_malloc(path, &s);
- if (r < 0)
- return r;
-
- return parse_uid(s, ret);
-}
-
-static int direct_lookup_uid(uid_t uid, char **ret) {
- char path[STRLEN("/run/systemd/dynamic-uid/direct:") + DECIMAL_STR_MAX(uid_t) + 1], *s;
- int r;
-
- xsprintf(path, "/run/systemd/dynamic-uid/direct:" UID_FMT, uid);
-
- r = readlink_malloc(path, &s);
- if (r < 0)
- return r;
- if (!valid_user_group_name(s)) { /* extra safety check */
- free(s);
- return -EINVAL;
- }
-
- *ret = s;
- return 0;
-}
+NSS_PWENT_PROTOTYPES(systemd);
+NSS_GRENT_PROTOTYPES(systemd);
+NSS_INITGROUPS_PROTOTYPE(systemd);
enum nss_status _nss_systemd_getpwnam_r(
const char *name,
@@ -139,99 +82,49 @@ enum nss_status _nss_systemd_getpwnam_r(
char *buffer, size_t buflen,
int *errnop) {
- _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
- _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL;
- _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- uint32_t translated;
- size_t l;
- int bypass, r;
+ enum nss_status status;
+ int e;
PROTECT_ERRNO;
BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
assert(name);
assert(pwd);
+ assert(errnop);
- /* If the username is not valid, then we don't know it. Ideally libc would filter these for us anyway. We don't
- * generate EINVAL here, because it isn't really out business to complain about invalid user names. */
+ /* If the username is not valid, then we don't know it. Ideally libc would filter these for us
+ * anyway. We don't generate EINVAL here, because it isn't really out business to complain about
+ * invalid user names. */
if (!valid_user_group_name(name))
return NSS_STATUS_NOTFOUND;
/* Synthesize entries for the root and nobody users, in case they are missing in /etc/passwd */
if (getenv_bool_secure("SYSTEMD_NSS_BYPASS_SYNTHETIC") <= 0) {
+
if (streq(name, root_passwd.pw_name)) {
*pwd = root_passwd;
return NSS_STATUS_SUCCESS;
}
- if (synthesize_nobody() &&
- streq(name, nobody_passwd.pw_name)) {
+
+ if (streq(name, nobody_passwd.pw_name)) {
+ if (!synthesize_nobody())
+ return NSS_STATUS_NOTFOUND;
+
*pwd = nobody_passwd;
return NSS_STATUS_SUCCESS;
}
- }
- /* 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)
+ } else if (STR_IN_SET(name, root_passwd.pw_name, nobody_passwd.pw_name))
return NSS_STATUS_NOTFOUND;
- bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS");
- if (bypass <= 0) {
- r = sd_bus_open_system(&bus);
- if (r < 0)
- bypass = 1;
- }
-
- if (bypass > 0) {
- r = direct_lookup_name(name, (uid_t*) &translated);
- if (r == -ENOENT)
- return NSS_STATUS_NOTFOUND;
- if (r < 0)
- goto fail;
- } else {
- r = sd_bus_call_method(bus,
- "org.freedesktop.systemd1",
- "/org/freedesktop/systemd1",
- "org.freedesktop.systemd1.Manager",
- "LookupDynamicUserByName",
- &error,
- &reply,
- "s",
- name);
- if (r < 0) {
- if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_DYNAMIC_USER))
- return NSS_STATUS_NOTFOUND;
-
- goto fail;
- }
-
- r = sd_bus_message_read(reply, "u", &translated);
- if (r < 0)
- goto fail;
- }
-
- l = strlen(name);
- if (buflen < l+1) {
+ status = userdb_getpwnam(name, pwd, buffer, buflen, &e);
+ if (IN_SET(status, NSS_STATUS_UNAVAIL, NSS_STATUS_TRYAGAIN)) {
UNPROTECT_ERRNO;
- *errnop = ERANGE;
- return NSS_STATUS_TRYAGAIN;
+ *errnop = e;
+ return status;
}
- memcpy(buffer, name, l+1);
-
- pwd->pw_name = buffer;
- pwd->pw_uid = (uid_t) translated;
- pwd->pw_gid = (uid_t) translated;
- pwd->pw_gecos = (char*) DYNAMIC_USER_GECOS;
- pwd->pw_passwd = (char*) DYNAMIC_USER_PASSWD;
- pwd->pw_dir = (char*) DYNAMIC_USER_DIR;
- pwd->pw_shell = (char*) DYNAMIC_USER_SHELL;
-
- return NSS_STATUS_SUCCESS;
-
-fail:
- UNPROTECT_ERRNO;
- *errnop = -r;
- return NSS_STATUS_UNAVAIL;
+ return status;
}
enum nss_status _nss_systemd_getpwuid_r(
@@ -240,100 +133,45 @@ enum nss_status _nss_systemd_getpwuid_r(
char *buffer, size_t buflen,
int *errnop) {
- _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
- _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL;
- _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- _cleanup_free_ char *direct = NULL;
- const char *translated;
- size_t l;
- int bypass, r;
+ enum nss_status status;
+ int e;
PROTECT_ERRNO;
BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
+ assert(pwd);
+ assert(errnop);
+
if (!uid_is_valid(uid))
return NSS_STATUS_NOTFOUND;
/* Synthesize data for the root user and for nobody in case they are missing from /etc/passwd */
if (getenv_bool_secure("SYSTEMD_NSS_BYPASS_SYNTHETIC") <= 0) {
+
if (uid == root_passwd.pw_uid) {
*pwd = root_passwd;
return NSS_STATUS_SUCCESS;
}
- if (synthesize_nobody() &&
- uid == nobody_passwd.pw_uid) {
+
+ if (uid == nobody_passwd.pw_uid) {
+ if (!synthesize_nobody())
+ return NSS_STATUS_NOTFOUND;
+
*pwd = nobody_passwd;
return NSS_STATUS_SUCCESS;
}
- }
- if (!uid_is_dynamic(uid))
+ } else if (uid == root_passwd.pw_uid || uid == nobody_passwd.pw_uid)
return NSS_STATUS_NOTFOUND;
- if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0)
- return NSS_STATUS_NOTFOUND;
-
- bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS");
- if (bypass <= 0) {
- r = sd_bus_open_system(&bus);
- if (r < 0)
- bypass = 1;
- }
-
- if (bypass > 0) {
- r = direct_lookup_uid(uid, &direct);
- if (r == -ENOENT)
- return NSS_STATUS_NOTFOUND;
- if (r < 0)
- goto fail;
-
- translated = direct;
-
- } else {
- r = sd_bus_call_method(bus,
- "org.freedesktop.systemd1",
- "/org/freedesktop/systemd1",
- "org.freedesktop.systemd1.Manager",
- "LookupDynamicUserByUID",
- &error,
- &reply,
- "u",
- (uint32_t) uid);
- if (r < 0) {
- if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_DYNAMIC_USER))
- return NSS_STATUS_NOTFOUND;
-
- goto fail;
- }
-
- r = sd_bus_message_read(reply, "s", &translated);
- if (r < 0)
- goto fail;
- }
-
- l = strlen(translated) + 1;
- if (buflen < l) {
+ status = userdb_getpwuid(uid, pwd, buffer, buflen, &e);
+ if (IN_SET(status, NSS_STATUS_UNAVAIL, NSS_STATUS_TRYAGAIN)) {
UNPROTECT_ERRNO;
- *errnop = ERANGE;
- return NSS_STATUS_TRYAGAIN;
+ *errnop = e;
+ return status;
}
- memcpy(buffer, translated, l);
-
- pwd->pw_name = buffer;
- pwd->pw_uid = uid;
- pwd->pw_gid = uid;
- pwd->pw_gecos = (char*) DYNAMIC_USER_GECOS;
- pwd->pw_passwd = (char*) DYNAMIC_USER_PASSWD;
- pwd->pw_dir = (char*) DYNAMIC_USER_DIR;
- pwd->pw_shell = (char*) DYNAMIC_USER_SHELL;
-
- return NSS_STATUS_SUCCESS;
-
-fail:
- UNPROTECT_ERRNO;
- *errnop = -r;
- return NSS_STATUS_UNAVAIL;
+ return status;
}
#pragma GCC diagnostic ignored "-Wsizeof-pointer-memaccess"
@@ -344,94 +182,46 @@ enum nss_status _nss_systemd_getgrnam_r(
char *buffer, size_t buflen,
int *errnop) {
- _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
- _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL;
- _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- uint32_t translated;
- size_t l;
- int bypass, r;
+ enum nss_status status;
+ int e;
PROTECT_ERRNO;
BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
assert(name);
assert(gr);
+ assert(errnop);
if (!valid_user_group_name(name))
return NSS_STATUS_NOTFOUND;
/* Synthesize records for root and nobody, in case they are missing form /etc/group */
if (getenv_bool_secure("SYSTEMD_NSS_BYPASS_SYNTHETIC") <= 0) {
+
if (streq(name, root_group.gr_name)) {
*gr = root_group;
return NSS_STATUS_SUCCESS;
}
- if (synthesize_nobody() &&
- streq(name, nobody_group.gr_name)) {
+
+ if (streq(name, nobody_group.gr_name)) {
+ if (!synthesize_nobody())
+ return NSS_STATUS_NOTFOUND;
+
*gr = nobody_group;
return NSS_STATUS_SUCCESS;
}
- }
- if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0)
+ } else if (STR_IN_SET(name, root_group.gr_name, nobody_group.gr_name))
return NSS_STATUS_NOTFOUND;
- bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS");
- if (bypass <= 0) {
- r = sd_bus_open_system(&bus);
- if (r < 0)
- bypass = 1;
- }
-
- if (bypass > 0) {
- r = direct_lookup_name(name, (uid_t*) &translated);
- if (r == -ENOENT)
- return NSS_STATUS_NOTFOUND;
- if (r < 0)
- goto fail;
- } else {
- r = sd_bus_call_method(bus,
- "org.freedesktop.systemd1",
- "/org/freedesktop/systemd1",
- "org.freedesktop.systemd1.Manager",
- "LookupDynamicUserByName",
- &error,
- &reply,
- "s",
- name);
- if (r < 0) {
- if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_DYNAMIC_USER))
- return NSS_STATUS_NOTFOUND;
-
- goto fail;
- }
-
- r = sd_bus_message_read(reply, "u", &translated);
- if (r < 0)
- goto fail;
- }
-
- l = sizeof(char*) + strlen(name) + 1;
- if (buflen < l) {
+ status = userdb_getgrnam(name, gr, buffer, buflen, &e);
+ if (IN_SET(status, NSS_STATUS_UNAVAIL, NSS_STATUS_TRYAGAIN)) {
UNPROTECT_ERRNO;
- *errnop = ERANGE;
- return NSS_STATUS_TRYAGAIN;
+ *errnop = e;
+ return status;
}
- memzero(buffer, sizeof(char*));
- strcpy(buffer + sizeof(char*), name);
-
- gr->gr_name = buffer + sizeof(char*);
- gr->gr_gid = (gid_t) translated;
- gr->gr_passwd = (char*) DYNAMIC_USER_PASSWD;
- gr->gr_mem = (char**) buffer;
-
- return NSS_STATUS_SUCCESS;
-
-fail:
- UNPROTECT_ERRNO;
- *errnop = -r;
- return NSS_STATUS_UNAVAIL;
+ return status;
}
enum nss_status _nss_systemd_getgrgid_r(
@@ -440,154 +230,56 @@ enum nss_status _nss_systemd_getgrgid_r(
char *buffer, size_t buflen,
int *errnop) {
- _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
- _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL;
- _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- _cleanup_free_ char *direct = NULL;
- const char *translated;
- size_t l;
- int bypass, r;
+ enum nss_status status;
+ int e;
PROTECT_ERRNO;
BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
+ assert(gr);
+ assert(errnop);
+
if (!gid_is_valid(gid))
return NSS_STATUS_NOTFOUND;
/* Synthesize records for root and nobody, in case they are missing from /etc/group */
if (getenv_bool_secure("SYSTEMD_NSS_BYPASS_SYNTHETIC") <= 0) {
+
if (gid == root_group.gr_gid) {
*gr = root_group;
return NSS_STATUS_SUCCESS;
}
- if (synthesize_nobody() &&
- gid == nobody_group.gr_gid) {
+
+ if (gid == nobody_group.gr_gid) {
+ if (!synthesize_nobody())
+ return NSS_STATUS_NOTFOUND;
+
*gr = nobody_group;
return NSS_STATUS_SUCCESS;
}
- }
- if (!gid_is_dynamic(gid))
+ } else if (gid == root_group.gr_gid || gid == nobody_group.gr_gid)
return NSS_STATUS_NOTFOUND;
- if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0)
- return NSS_STATUS_NOTFOUND;
-
- bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS");
- if (bypass <= 0) {
- r = sd_bus_open_system(&bus);
- if (r < 0)
- bypass = 1;
- }
-
- if (bypass > 0) {
- r = direct_lookup_uid(gid, &direct);
- if (r == -ENOENT)
- return NSS_STATUS_NOTFOUND;
- if (r < 0)
- goto fail;
-
- translated = direct;
-
- } else {
- r = sd_bus_call_method(bus,
- "org.freedesktop.systemd1",
- "/org/freedesktop/systemd1",
- "org.freedesktop.systemd1.Manager",
- "LookupDynamicUserByUID",
- &error,
- &reply,
- "u",
- (uint32_t) gid);
- if (r < 0) {
- if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_DYNAMIC_USER))
- return NSS_STATUS_NOTFOUND;
-
- goto fail;
- }
-
- r = sd_bus_message_read(reply, "s", &translated);
- if (r < 0)
- goto fail;
- }
-
- l = sizeof(char*) + strlen(translated) + 1;
- if (buflen < l) {
+ status = userdb_getgrgid(gid, gr, buffer, buflen, &e);
+ if (IN_SET(status, NSS_STATUS_UNAVAIL, NSS_STATUS_TRYAGAIN)) {
UNPROTECT_ERRNO;
- *errnop = ERANGE;
- return NSS_STATUS_TRYAGAIN;
+ *errnop = e;
+ return status;
}
- memzero(buffer, sizeof(char*));
- strcpy(buffer + sizeof(char*), translated);
-
- gr->gr_name = buffer + sizeof(char*);
- gr->gr_gid = gid;
- gr->gr_passwd = (char*) DYNAMIC_USER_PASSWD;
- gr->gr_mem = (char**) buffer;
-
- return NSS_STATUS_SUCCESS;
-
-fail:
- UNPROTECT_ERRNO;
- *errnop = -r;
- return NSS_STATUS_UNAVAIL;
-}
-
-static void user_entry_free(UserEntry *p) {
- if (!p)
- return;
-
- if (p->data)
- LIST_REMOVE(entries, p->data->entries, p);
-
- free(p->name);
- free(p);
-}
-
-static int user_entry_add(GetentData *data, const char *name, uid_t id) {
- UserEntry *p;
-
- assert(data);
-
- /* This happens when User= or Group= already exists statically. */
- if (!uid_is_dynamic(id))
- return -EINVAL;
-
- p = new0(UserEntry, 1);
- if (!p)
- return -ENOMEM;
-
- p->name = strdup(name);
- if (!p->name) {
- free(p);
- return -ENOMEM;
- }
- p->id = id;
- p->data = data;
-
- LIST_PREPEND(entries, data->entries, p);
-
- return 0;
-}
-
-static void systemd_endent(GetentData *data) {
- UserEntry *p;
-
- assert(data);
-
- while ((p = data->entries))
- user_entry_free(p);
-
- data->position = NULL;
+ return status;
}
static enum nss_status nss_systemd_endent(GetentData *p) {
PROTECT_ERRNO;
BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
+ assert(p);
+
assert_se(pthread_mutex_lock(&p->mutex) == 0);
- systemd_endent(p);
+ p->iterator = userdb_iterator_free(p->iterator);
+ p->by_membership = false;
assert_se(pthread_mutex_unlock(&p->mutex) == 0);
return NSS_STATUS_SUCCESS;
@@ -601,235 +293,353 @@ enum nss_status _nss_systemd_endgrent(void) {
return nss_systemd_endent(&getgrent_data);
}
-static int direct_enumeration(GetentData *p) {
- _cleanup_closedir_ DIR *d = NULL;
- struct dirent *de;
- int r;
-
- assert(p);
-
- d = opendir("/run/systemd/dynamic-uid/");
- if (!d)
- return -errno;
-
- FOREACH_DIRENT(de, d, return -errno) {
- _cleanup_free_ char *name = NULL;
- uid_t uid, verified;
-
- if (!dirent_is_file(de))
- continue;
-
- r = parse_uid(de->d_name, &uid);
- if (r < 0)
- continue;
-
- r = direct_lookup_uid(uid, &name);
- if (r == -ENOMEM)
- return r;
- if (r < 0)
- continue;
-
- r = direct_lookup_name(name, &verified);
- if (r < 0)
- continue;
-
- if (uid != verified)
- continue;
-
- r = user_entry_add(p, name, uid);
- if (r == -ENOMEM)
- return r;
- if (r < 0)
- continue;
- }
-
- return 0;
-}
-
-static enum nss_status systemd_setent(GetentData *p) {
- _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
- _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL;
- _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- const char *name;
- uid_t id;
- int bypass, r;
-
- PROTECT_ERRNO;
- BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
-
- assert(p);
-
- assert_se(pthread_mutex_lock(&p->mutex) == 0);
-
- systemd_endent(p);
-
- if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0)
- goto finish;
-
- bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS");
-
- if (bypass <= 0) {
- r = sd_bus_open_system(&bus);
- if (r < 0)
- bypass = 1;
- }
-
- if (bypass > 0) {
- r = direct_enumeration(p);
- if (r < 0)
- goto fail;
-
- goto finish;
- }
-
- r = sd_bus_call_method(bus,
- "org.freedesktop.systemd1",
- "/org/freedesktop/systemd1",
- "org.freedesktop.systemd1.Manager",
- "GetDynamicUsers",
- &error,
- &reply,
- NULL);
- if (r < 0)
- goto fail;
-
- r = sd_bus_message_enter_container(reply, 'a', "(us)");
- if (r < 0)
- goto fail;
-
- while ((r = sd_bus_message_read(reply, "(us)", &id, &name)) > 0) {
- r = user_entry_add(p, name, id);
- if (r == -ENOMEM)
- goto fail;
- if (r < 0)
- continue;
- }
- if (r < 0)
- goto fail;
-
- r = sd_bus_message_exit_container(reply);
- if (r < 0)
- goto fail;
-
-finish:
- p->position = p->entries;
- assert_se(pthread_mutex_unlock(&p->mutex) == 0);
-
- return NSS_STATUS_SUCCESS;
-
-fail:
- systemd_endent(p);
- assert_se(pthread_mutex_unlock(&p->mutex) == 0);
-
- return NSS_STATUS_UNAVAIL;
-}
-
enum nss_status _nss_systemd_setpwent(int stayopen) {
- return systemd_setent(&getpwent_data);
-}
-
-enum nss_status _nss_systemd_setgrent(int stayopen) {
- return systemd_setent(&getgrent_data);
-}
-
-enum nss_status _nss_systemd_getpwent_r(struct passwd *result, char *buffer, size_t buflen, int *errnop) {
enum nss_status ret;
- UserEntry *p;
- size_t len;
PROTECT_ERRNO;
BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
- assert(result);
- assert(buffer);
- assert(errnop);
+ if (userdb_nss_compat_is_enabled() <= 0)
+ return NSS_STATUS_NOTFOUND;
assert_se(pthread_mutex_lock(&getpwent_data.mutex) == 0);
- LIST_FOREACH(entries, p, getpwent_data.position) {
- len = strlen(p->name) + 1;
- if (buflen < len) {
- UNPROTECT_ERRNO;
- *errnop = ERANGE;
- ret = NSS_STATUS_TRYAGAIN;
- goto finalize;
- }
+ getpwent_data.iterator = userdb_iterator_free(getpwent_data.iterator);
+ getpwent_data.by_membership = false;
- memcpy(buffer, p->name, len);
-
- result->pw_name = buffer;
- result->pw_uid = p->id;
- result->pw_gid = p->id;
- result->pw_gecos = (char*) DYNAMIC_USER_GECOS;
- result->pw_passwd = (char*) DYNAMIC_USER_PASSWD;
- result->pw_dir = (char*) DYNAMIC_USER_DIR;
- result->pw_shell = (char*) DYNAMIC_USER_SHELL;
- break;
- }
- if (!p) {
- ret = NSS_STATUS_NOTFOUND;
- goto finalize;
- }
-
- /* On success, step to the next entry. */
- p = p->entries_next;
- ret = NSS_STATUS_SUCCESS;
-
-finalize:
- /* Save position for the next call. */
- getpwent_data.position = p;
+ ret = userdb_all(nss_glue_userdb_flags(), &getpwent_data.iterator) < 0 ?
+ NSS_STATUS_UNAVAIL : NSS_STATUS_SUCCESS;
assert_se(pthread_mutex_unlock(&getpwent_data.mutex) == 0);
-
return ret;
}
-enum nss_status _nss_systemd_getgrent_r(struct group *result, char *buffer, size_t buflen, int *errnop) {
+enum nss_status _nss_systemd_setgrent(int stayopen) {
enum nss_status ret;
- UserEntry *p;
- size_t len;
+
+ PROTECT_ERRNO;
+ BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
+
+ if (userdb_nss_compat_is_enabled() <= 0)
+ return NSS_STATUS_NOTFOUND;
+
+ assert_se(pthread_mutex_lock(&getgrent_data.mutex) == 0);
+
+ getgrent_data.iterator = userdb_iterator_free(getgrent_data.iterator);
+ getpwent_data.by_membership = false;
+
+ ret = groupdb_all(nss_glue_userdb_flags(), &getgrent_data.iterator) < 0 ?
+ NSS_STATUS_UNAVAIL : NSS_STATUS_SUCCESS;
+
+ assert_se(pthread_mutex_unlock(&getgrent_data.mutex) == 0);
+ return ret;
+}
+
+enum nss_status _nss_systemd_getpwent_r(
+ struct passwd *result,
+ char *buffer, size_t buflen,
+ int *errnop) {
+
+ _cleanup_(user_record_unrefp) UserRecord *ur = NULL;
+ enum nss_status ret;
+ int r;
PROTECT_ERRNO;
BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
assert(result);
- assert(buffer);
assert(errnop);
+ r = userdb_nss_compat_is_enabled();
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+ if (!r)
+ return NSS_STATUS_NOTFOUND;
+
+ assert_se(pthread_mutex_lock(&getpwent_data.mutex) == 0);
+
+ if (!getpwent_data.iterator) {
+ UNPROTECT_ERRNO;
+ *errnop = EHOSTDOWN;
+ ret = NSS_STATUS_UNAVAIL;
+ goto finish;
+ }
+
+ r = userdb_iterator_get(getpwent_data.iterator, &ur);
+ if (r == -ESRCH) {
+ ret = NSS_STATUS_NOTFOUND;
+ goto finish;
+ }
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ ret = NSS_STATUS_UNAVAIL;
+ goto finish;
+ }
+
+ r = nss_pack_user_record(ur, result, buffer, buflen);
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ ret = NSS_STATUS_TRYAGAIN;
+ goto finish;
+ }
+
+ ret = NSS_STATUS_SUCCESS;
+
+finish:
+ assert_se(pthread_mutex_unlock(&getpwent_data.mutex) == 0);
+ return ret;
+}
+
+enum nss_status _nss_systemd_getgrent_r(
+ struct group *result,
+ char *buffer, size_t buflen,
+ int *errnop) {
+
+ _cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
+ _cleanup_free_ char **members = NULL;
+ enum nss_status ret;
+ int r;
+
+ PROTECT_ERRNO;
+ BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
+
+ assert(result);
+ assert(errnop);
+
+ r = userdb_nss_compat_is_enabled();
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+ if (!r)
+ return NSS_STATUS_UNAVAIL;
+
assert_se(pthread_mutex_lock(&getgrent_data.mutex) == 0);
- LIST_FOREACH(entries, p, getgrent_data.position) {
- len = sizeof(char*) + strlen(p->name) + 1;
- if (buflen < len) {
+ if (!getgrent_data.iterator) {
+ UNPROTECT_ERRNO;
+ *errnop = EHOSTDOWN;
+ ret = NSS_STATUS_UNAVAIL;
+ goto finish;
+ }
+
+ if (!getgrent_data.by_membership) {
+ r = groupdb_iterator_get(getgrent_data.iterator, &gr);
+ if (r == -ESRCH) {
+ /* So we finished iterating native groups now. let's now continue with iterating
+ * native memberships, and generate additional group entries for any groups
+ * referenced there that are defined in NSS only. This means for those groups there
+ * will be two or more entries generated during iteration, but this is apparently how
+ * this is supposed to work, and what other implementations do too. Clients are
+ * supposed to merge the group records found during iteration automatically. */
+ getgrent_data.iterator = userdb_iterator_free(getgrent_data.iterator);
+
+ r = membershipdb_all(nss_glue_userdb_flags(), &getgrent_data.iterator);
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ ret = NSS_STATUS_UNAVAIL;
+ goto finish;
+ }
+
+ getgrent_data.by_membership = true;
+ } else if (r < 0) {
UNPROTECT_ERRNO;
- *errnop = ERANGE;
- ret = NSS_STATUS_TRYAGAIN;
- goto finalize;
+ *errnop = -r;
+ ret = NSS_STATUS_UNAVAIL;
+ goto finish;
+ } else if (!STR_IN_SET(gr->group_name, root_group.gr_name, nobody_group.gr_name)) {
+ r = membershipdb_by_group_strv(gr->group_name, nss_glue_userdb_flags(), &members);
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ ret = NSS_STATUS_UNAVAIL;
+ goto finish;
+ }
}
-
- memzero(buffer, sizeof(char*));
- strcpy(buffer + sizeof(char*), p->name);
-
- result->gr_name = buffer + sizeof(char*);
- result->gr_gid = p->id;
- result->gr_passwd = (char*) DYNAMIC_USER_PASSWD;
- result->gr_mem = (char**) buffer;
- break;
- }
- if (!p) {
- ret = NSS_STATUS_NOTFOUND;
- goto finalize;
}
- /* On success, step to the next entry. */
- p = p->entries_next;
+ if (getgrent_data.by_membership) {
+ _cleanup_close_ int lock_fd = -1;
+
+ for (;;) {
+ _cleanup_free_ char *user_name = NULL, *group_name = NULL;
+
+ r = membershipdb_iterator_get(getgrent_data.iterator, &user_name, &group_name);
+ if (r == -ESRCH) {
+ ret = NSS_STATUS_NOTFOUND;
+ goto finish;
+ }
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ ret = NSS_STATUS_UNAVAIL;
+ goto finish;
+ }
+
+ if (STR_IN_SET(user_name, root_passwd.pw_name, nobody_passwd.pw_name))
+ continue;
+ if (STR_IN_SET(group_name, root_group.gr_name, nobody_group.gr_name))
+ continue;
+
+ /* We are about to recursively call into NSS, let's make sure we disable recursion into our own code. */
+ if (lock_fd < 0) {
+ lock_fd = userdb_nss_compat_disable();
+ if (lock_fd < 0 && lock_fd != -EBUSY) {
+ UNPROTECT_ERRNO;
+ *errnop = -lock_fd;
+ ret = NSS_STATUS_UNAVAIL;
+ goto finish;
+ }
+ }
+
+ r = nss_group_record_by_name(group_name, &gr);
+ if (r == -ESRCH)
+ continue;
+ if (r < 0) {
+ log_debug_errno(r, "Failed to do NSS check for group '%s', ignoring: %m", group_name);
+ continue;
+ }
+
+ members = strv_new(user_name);
+ if (!members) {
+ UNPROTECT_ERRNO;
+ *errnop = ENOMEM;
+ return NSS_STATUS_TRYAGAIN;
+ }
+
+ /* Note that we currently generate one group entry per user that is part of a
+ * group. It's a bit ugly, but equivalent to generating a single entry with a set of
+ * members in them. */
+ break;
+ }
+ }
+
+ r = nss_pack_group_record(gr, members, result, buffer, buflen);
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ ret = NSS_STATUS_TRYAGAIN;
+ goto finish;
+ }
+
ret = NSS_STATUS_SUCCESS;
-finalize:
- /* Save position for the next call. */
- getgrent_data.position = p;
-
+finish:
assert_se(pthread_mutex_unlock(&getgrent_data.mutex) == 0);
-
return ret;
}
+
+enum nss_status _nss_systemd_initgroups_dyn(
+ const char *user_name,
+ gid_t gid,
+ long *start,
+ long *size,
+ gid_t **groupsp,
+ long int limit,
+ int *errnop) {
+
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ bool any = false;
+ int r;
+
+ PROTECT_ERRNO;
+ BLOCK_SIGNALS(NSS_SIGNALS_BLOCK);
+
+ assert(user_name);
+ assert(start);
+ assert(size);
+ assert(groupsp);
+ assert(errnop);
+
+ if (!valid_user_group_name(user_name))
+ return NSS_STATUS_NOTFOUND;
+
+ /* Don't allow extending these two special users, the same as we won't resolve them via getpwnam() */
+ if (STR_IN_SET(user_name, root_passwd.pw_name, nobody_passwd.pw_name))
+ return NSS_STATUS_NOTFOUND;
+
+ r = userdb_nss_compat_is_enabled();
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+ if (!r)
+ return NSS_STATUS_NOTFOUND;
+
+ r = membershipdb_by_user(user_name, nss_glue_userdb_flags(), &iterator);
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+
+ for (;;) {
+ _cleanup_(group_record_unrefp) GroupRecord *g = NULL;
+ _cleanup_free_ char *group_name = NULL;
+
+ r = membershipdb_iterator_get(iterator, NULL, &group_name);
+ if (r == -ESRCH)
+ break;
+ if (r < 0) {
+ UNPROTECT_ERRNO;
+ *errnop = -r;
+ return NSS_STATUS_UNAVAIL;
+ }
+
+ /* The group might be defined via traditional NSS only, hence let's do a full look-up without
+ * disabling NSS. This means we are operating recursively here. */
+
+ r = groupdb_by_name(group_name, nss_glue_userdb_flags() & ~USERDB_AVOID_NSS, &g);
+ if (r == -ESRCH)
+ continue;
+ if (r < 0) {
+ log_debug_errno(r, "Failed to resolve group '%s', ignoring: %m", group_name);
+ continue;
+ }
+
+ if (g->gid == gid)
+ continue;
+
+ if (*start >= *size) {
+ gid_t *new_groups;
+ long new_size;
+
+ if (limit > 0 && *size >= limit) /* Reached the limit.? */
+ break;
+
+ if (*size > LONG_MAX/2) { /* Check for overflow */
+ UNPROTECT_ERRNO;
+ *errnop = ENOMEM;
+ return NSS_STATUS_TRYAGAIN;
+ }
+
+ new_size = *start * 2;
+ if (limit > 0 && new_size > limit)
+ new_size = limit;
+
+ /* Enlarge buffer */
+ new_groups = realloc(*groupsp, new_size * sizeof(**groupsp));
+ if (!new_groups) {
+ UNPROTECT_ERRNO;
+ *errnop = ENOMEM;
+ return NSS_STATUS_TRYAGAIN;
+ }
+
+ *groupsp = new_groups;
+ *size = new_size;
+ }
+
+ (*groupsp)[(*start)++] = g->gid;
+ any = true;
+ }
+
+ return any ? NSS_STATUS_SUCCESS : NSS_STATUS_NOTFOUND;
+}
diff --git a/src/nss-systemd/nss-systemd.sym b/src/nss-systemd/nss-systemd.sym
index ff63382b15..77e1fbe93f 100644
--- a/src/nss-systemd/nss-systemd.sym
+++ b/src/nss-systemd/nss-systemd.sym
@@ -19,5 +19,6 @@ global:
_nss_systemd_endgrent;
_nss_systemd_setgrent;
_nss_systemd_getgrent_r;
+ _nss_systemd_initgroups_dyn;
local: *;
};
diff --git a/src/nss-systemd/userdb-glue.c b/src/nss-systemd/userdb-glue.c
new file mode 100644
index 0000000000..81705fa1b6
--- /dev/null
+++ b/src/nss-systemd/userdb-glue.c
@@ -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;
+}
diff --git a/src/nss-systemd/userdb-glue.h b/src/nss-systemd/userdb-glue.h
new file mode 100644
index 0000000000..02add24b6b
--- /dev/null
+++ b/src/nss-systemd/userdb-glue.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#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);
diff --git a/src/shared/group-record-nss.c b/src/shared/group-record-nss.c
new file mode 100644
index 0000000000..77924f1c40
--- /dev/null
+++ b/src/shared/group-record-nss.c
@@ -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;
+}
diff --git a/src/shared/group-record-nss.h b/src/shared/group-record-nss.h
new file mode 100644
index 0000000000..38b2995178
--- /dev/null
+++ b/src/shared/group-record-nss.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+#include
+
+#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);
diff --git a/src/shared/group-record-show.c b/src/shared/group-record-show.c
new file mode 100644
index 0000000000..d0300e483c
--- /dev/null
+++ b/src/shared/group-record-show.c
@@ -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);
+}
diff --git a/src/shared/group-record-show.h b/src/shared/group-record-show.h
new file mode 100644
index 0000000000..12bdbd1724
--- /dev/null
+++ b/src/shared/group-record-show.h
@@ -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);
diff --git a/src/shared/group-record.c b/src/shared/group-record.c
new file mode 100644
index 0000000000..d5ec32f07d
--- /dev/null
+++ b/src/shared/group-record.c
@@ -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;
+}
diff --git a/src/shared/group-record.h b/src/shared/group-record.h
new file mode 100644
index 0000000000..b72a43e50d
--- /dev/null
+++ b/src/shared/group-record.h
@@ -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);
diff --git a/src/shared/libcrypt-util.c b/src/shared/libcrypt-util.c
new file mode 100644
index 0000000000..f41685ae45
--- /dev/null
+++ b/src/shared/libcrypt-util.c
@@ -0,0 +1,86 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+#include
+
+#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, "$", "!$");
+}
diff --git a/src/shared/libcrypt-util.h b/src/shared/libcrypt-util.h
new file mode 100644
index 0000000000..93f0e13ffb
--- /dev/null
+++ b/src/shared/libcrypt-util.h
@@ -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
+#endif
+
+#include
+#include
+
+int make_salt(char **ret);
+
+bool hashed_password_valid(const char *s);
diff --git a/src/shared/meson.build b/src/shared/meson.build
index 08aa480b6c..a7320fc4ed 100644
--- a/src/shared/meson.build
+++ b/src/shared/meson.build
@@ -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,
diff --git a/src/shared/pam-util.c b/src/shared/pam-util.c
new file mode 100644
index 0000000000..f000798ce0
--- /dev/null
+++ b/src/shared/pam-util.c
@@ -0,0 +1,81 @@
+#include
+#include
+#include
+
+#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);
+}
diff --git a/src/shared/pam-util.h b/src/shared/pam-util.h
new file mode 100644
index 0000000000..26d07b7f0c
--- /dev/null
+++ b/src/shared/pam-util.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+
+#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);
diff --git a/src/shared/user-record-nss.c b/src/shared/user-record-nss.c
new file mode 100644
index 0000000000..11c085d63d
--- /dev/null
+++ b/src/shared/user-record-nss.c
@@ -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;
+}
diff --git a/src/shared/user-record-nss.h b/src/shared/user-record-nss.h
new file mode 100644
index 0000000000..d5fb23ad2a
--- /dev/null
+++ b/src/shared/user-record-nss.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+#include
+
+#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);
diff --git a/src/shared/user-record-show.c b/src/shared/user-record-show.c
new file mode 100644
index 0000000000..e5eff50840
--- /dev/null
+++ b/src/shared/user-record-show.c
@@ -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);
+}
diff --git a/src/shared/user-record-show.h b/src/shared/user-record-show.h
new file mode 100644
index 0000000000..bd22be2ae0
--- /dev/null
+++ b/src/shared/user-record-show.h
@@ -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);
diff --git a/src/shared/user-record.c b/src/shared/user-record.c
new file mode 100644
index 0000000000..327cf8da87
--- /dev/null
+++ b/src/shared/user-record.c
@@ -0,0 +1,1883 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+
+#include "cgroup-util.h"
+#include "dns-domain.h"
+#include "env-util.h"
+#include "fs-util.h"
+#include "hexdecoct.h"
+#include "hostname-util.h"
+#include "memory-util.h"
+#include "path-util.h"
+#include "pkcs11-util.h"
+#include "rlimit-util.h"
+#include "stat-util.h"
+#include "string-table.h"
+#include "strv.h"
+#include "user-record.h"
+#include "user-util.h"
+
+#define DEFAULT_RATELIMIT_BURST 30
+#define DEFAULT_RATELIMIT_INTERVAL_USEC (1*USEC_PER_MINUTE)
+
+UserRecord* user_record_new(void) {
+ UserRecord *h;
+
+ h = new(UserRecord, 1);
+ if (!h)
+ return NULL;
+
+ *h = (UserRecord) {
+ .n_ref = 1,
+ .disposition = _USER_DISPOSITION_INVALID,
+ .last_change_usec = UINT64_MAX,
+ .last_password_change_usec = UINT64_MAX,
+ .umask = MODE_INVALID,
+ .nice_level = INT_MAX,
+ .not_before_usec = UINT64_MAX,
+ .not_after_usec = UINT64_MAX,
+ .locked = -1,
+ .storage = _USER_STORAGE_INVALID,
+ .access_mode = MODE_INVALID,
+ .disk_size = UINT64_MAX,
+ .disk_size_relative = UINT64_MAX,
+ .tasks_max = UINT64_MAX,
+ .memory_high = UINT64_MAX,
+ .memory_max = UINT64_MAX,
+ .cpu_weight = UINT64_MAX,
+ .io_weight = UINT64_MAX,
+ .uid = UID_INVALID,
+ .gid = GID_INVALID,
+ .nodev = true,
+ .nosuid = true,
+ .luks_discard = -1,
+ .luks_volume_key_size = UINT64_MAX,
+ .luks_pbkdf_time_cost_usec = UINT64_MAX,
+ .luks_pbkdf_memory_cost = UINT64_MAX,
+ .luks_pbkdf_parallel_threads = UINT64_MAX,
+ .disk_usage = UINT64_MAX,
+ .disk_free = UINT64_MAX,
+ .disk_ceiling = UINT64_MAX,
+ .disk_floor = UINT64_MAX,
+ .signed_locally = -1,
+ .good_authentication_counter = UINT64_MAX,
+ .bad_authentication_counter = UINT64_MAX,
+ .last_good_authentication_usec = UINT64_MAX,
+ .last_bad_authentication_usec = UINT64_MAX,
+ .ratelimit_begin_usec = UINT64_MAX,
+ .ratelimit_count = UINT64_MAX,
+ .ratelimit_interval_usec = UINT64_MAX,
+ .ratelimit_burst = UINT64_MAX,
+ .removable = -1,
+ .enforce_password_policy = -1,
+ .auto_login = -1,
+ .stop_delay_usec = UINT64_MAX,
+ .kill_processes = -1,
+ .password_change_min_usec = UINT64_MAX,
+ .password_change_max_usec = UINT64_MAX,
+ .password_change_warn_usec = UINT64_MAX,
+ .password_change_inactive_usec = UINT64_MAX,
+ .password_change_now = -1,
+ .pkcs11_protected_authentication_path_permitted = -1,
+ };
+
+ return h;
+}
+
+static void pkcs11_encrypted_key_done(Pkcs11EncryptedKey *k) {
+ if (!k)
+ return;
+
+ free(k->uri);
+ erase_and_free(k->data);
+ erase_and_free(k->hashed_password);
+}
+
+static UserRecord* user_record_free(UserRecord *h) {
+ if (!h)
+ return NULL;
+
+ free(h->user_name);
+ free(h->realm);
+ free(h->user_name_and_realm_auto);
+ free(h->real_name);
+ free(h->email_address);
+ erase_and_free(h->password_hint);
+ free(h->location);
+ free(h->icon_name);
+
+ free(h->shell);
+
+ strv_free(h->environment);
+ free(h->time_zone);
+ free(h->preferred_language);
+ rlimit_free_all(h->rlimits);
+
+ free(h->skeleton_directory);
+
+ strv_free_erase(h->hashed_password);
+ strv_free_erase(h->ssh_authorized_keys);
+ strv_free_erase(h->password);
+ strv_free_erase(h->pkcs11_pin);
+
+ free(h->cifs_service);
+ free(h->cifs_user_name);
+ free(h->cifs_domain);
+
+ free(h->image_path);
+ free(h->image_path_auto);
+ free(h->home_directory);
+ free(h->home_directory_auto);
+
+ strv_free(h->member_of);
+
+ free(h->file_system_type);
+ free(h->luks_cipher);
+ free(h->luks_cipher_mode);
+ free(h->luks_pbkdf_hash_algorithm);
+ free(h->luks_pbkdf_type);
+
+ free(h->state);
+ free(h->service);
+
+ strv_free(h->pkcs11_token_uri);
+ for (size_t i = 0; i < h->n_pkcs11_encrypted_key; i++)
+ pkcs11_encrypted_key_done(h->pkcs11_encrypted_key + i);
+ free(h->pkcs11_encrypted_key);
+
+ json_variant_unref(h->json);
+
+ return mfree(h);
+}
+
+DEFINE_TRIVIAL_REF_UNREF_FUNC(UserRecord, user_record, user_record_free);
+
+int json_dispatch_realm(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ char **s = userdata;
+ const char *n;
+ int r;
+
+ if (json_variant_is_null(variant)) {
+ *s = mfree(*s);
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ n = json_variant_string(variant);
+ r = dns_name_is_valid(n);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to check if JSON field '%s' is a valid DNS domain.", strna(name));
+ if (r == 0)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid DNS domain.", strna(name));
+
+ r = free_and_strdup(s, n);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to allocate string: %m");
+
+ return 0;
+}
+
+static int json_dispatch_gecos(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ char **s = userdata;
+ const char *n;
+ int r;
+
+ if (json_variant_is_null(variant)) {
+ *s = mfree(*s);
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ n = json_variant_string(variant);
+ if (!valid_gecos(n))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid GECOS compatible real name.", strna(name));
+
+ r = free_and_strdup(s, n);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to allocate string: %m");
+
+ return 0;
+}
+
+static int json_dispatch_nice(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ int *nl = userdata;
+ intmax_t m;
+
+ if (json_variant_is_null(variant)) {
+ *nl = INT_MAX;
+ return 0;
+ }
+
+ if (!json_variant_is_integer(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ m = json_variant_integer(variant);
+ if (m < PRIO_MIN || m >= PRIO_MAX)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "JSON field '%s' is not a valid nice level.", strna(name));
+
+ *nl = m;
+ return 0;
+}
+
+static int json_dispatch_rlimit_value(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ rlim_t *ret = userdata;
+
+ if (json_variant_is_null(variant))
+ *ret = RLIM_INFINITY;
+ else if (json_variant_is_unsigned(variant)) {
+ uintmax_t w;
+
+ w = json_variant_unsigned(variant);
+ if (w == RLIM_INFINITY || (uintmax_t) w != json_variant_unsigned(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "Resource limit value '%s' is out of range.", name);
+
+ *ret = (rlim_t) w;
+ } else
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit value '%s' is not an unsigned integer.", name);
+
+ return 0;
+}
+
+static int json_dispatch_rlimits(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ struct rlimit** limits = userdata;
+ JsonVariant *value;
+ const char *key;
+ int r;
+
+ assert_se(limits);
+
+ if (json_variant_is_null(variant)) {
+ rlimit_free_all(limits);
+ 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));
+
+ JSON_VARIANT_OBJECT_FOREACH(key, value, variant) {
+ JsonVariant *jcur, *jmax;
+ struct rlimit rl;
+ const char *p;
+ int l;
+
+ p = startswith(key, "RLIMIT_");
+ if (!p)
+ l = -1;
+ else
+ l = rlimit_from_string(p);
+ if (l < 0)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' not known.", key);
+
+ if (!json_variant_is_object(value))
+ return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' has invalid value.", key);
+
+ if (json_variant_elements(value) != 4)
+ return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' value is does not have two fields as expected.", key);
+
+ jcur = json_variant_by_key(value, "cur");
+ if (!jcur)
+ return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' lacks 'cur' field.", key);
+ r = json_dispatch_rlimit_value("cur", jcur, flags, &rl.rlim_cur);
+ if (r < 0)
+ return r;
+
+ jmax = json_variant_by_key(value, "max");
+ if (!jmax)
+ return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' lacks 'max' field.", key);
+ r = json_dispatch_rlimit_value("max", jmax, flags, &rl.rlim_max);
+ if (r < 0)
+ return r;
+
+ if (limits[l])
+ *(limits[l]) = rl;
+ else {
+ limits[l] = newdup(struct rlimit, &rl, 1);
+ if (!limits[l])
+ return log_oom();
+ }
+ }
+
+ return 0;
+}
+
+static int json_dispatch_filename_or_path(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ char **s = userdata;
+ const char *n;
+ int r;
+
+ assert(s);
+
+ if (json_variant_is_null(variant)) {
+ *s = mfree(*s);
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ n = json_variant_string(variant);
+ if (!filename_is_valid(n) && !path_is_normalized(n))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid file name or normalized path.", strna(name));
+
+ r = free_and_strdup(s, n);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to allocate string: %m");
+
+ return 0;
+}
+
+static int json_dispatch_path(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ char **s = userdata;
+ const char *n;
+ int r;
+
+ if (json_variant_is_null(variant)) {
+ *s = mfree(*s);
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ n = json_variant_string(variant);
+ if (!path_is_normalized(n))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a normalized file system path.", strna(name));
+ if (!path_is_absolute(n))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an absolute file system path.", strna(name));
+
+ r = free_and_strdup(s, n);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to allocate string: %m");
+
+ return 0;
+}
+
+static int json_dispatch_home_directory(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ char **s = userdata;
+ const char *n;
+ int r;
+
+ if (json_variant_is_null(variant)) {
+ *s = mfree(*s);
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ n = json_variant_string(variant);
+ if (!valid_home(n))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid home directory path.", strna(name));
+
+ r = free_and_strdup(s, n);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to allocate string: %m");
+
+ return 0;
+}
+
+static int json_dispatch_image_path(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ char **s = userdata;
+ const char *n;
+ int r;
+
+ if (json_variant_is_null(variant)) {
+ *s = mfree(*s);
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ n = json_variant_string(variant);
+ if (empty_or_root(n) || !path_is_valid(n) || !path_is_absolute(n))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid image path.", strna(name));
+
+ r = free_and_strdup(s, n);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to allocate string: %m");
+
+ return 0;
+}
+
+static int json_dispatch_umask(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ mode_t *m = userdata;
+ uintmax_t k;
+
+ if (json_variant_is_null(variant)) {
+ *m = (mode_t) -1;
+ return 0;
+ }
+
+ if (!json_variant_is_unsigned(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a number.", strna(name));
+
+ k = json_variant_unsigned(variant);
+ if (k > 0777)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' outside of valid range 0…0777.", strna(name));
+
+ *m = (mode_t) k;
+ return 0;
+}
+
+static int json_dispatch_access_mode(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ mode_t *m = userdata;
+ uintmax_t k;
+
+ if (json_variant_is_null(variant)) {
+ *m = (mode_t) -1;
+ return 0;
+ }
+
+ if (!json_variant_is_unsigned(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a number.", strna(name));
+
+ k = json_variant_unsigned(variant);
+ if (k > 07777)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' outside of valid range 0…07777.", strna(name));
+
+ *m = (mode_t) k;
+ return 0;
+}
+
+static int json_dispatch_environment(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ _cleanup_strv_free_ char **n = NULL;
+ char ***l = userdata;
+ size_t i;
+ int r;
+
+ if (json_variant_is_null(variant)) {
+ *l = strv_free(*l);
+ 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));
+
+ for (i = 0; i < json_variant_elements(variant); i++) {
+ _cleanup_free_ char *c = NULL;
+ JsonVariant *e;
+ const char *a;
+
+ e = json_variant_by_index(variant, i);
+ if (!json_variant_is_string(e))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of strings.", strna(name));
+
+ assert_se(a = json_variant_string(e));
+
+ if (!env_assignment_is_valid(a))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of environment variables.", strna(name));
+
+ c = strdup(a);
+ if (!c)
+ return json_log_oom(variant, flags);
+
+ r = strv_env_replace(&n, c);
+ if (r < 0)
+ return json_log_oom(variant, flags);
+
+ c = NULL;
+ }
+
+ strv_free_and_replace(*l, n);
+ return 0;
+}
+
+int json_dispatch_user_disposition(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ UserDisposition *disposition = userdata, k;
+
+ if (json_variant_is_null(variant)) {
+ *disposition = _USER_DISPOSITION_INVALID;
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ k = user_disposition_from_string(json_variant_string(variant));
+ if (k < 0)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Disposition type '%s' not known.", json_variant_string(variant));
+
+ *disposition = k;
+ return 0;
+}
+
+static int json_dispatch_storage(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ UserStorage *storage = userdata, k;
+
+ if (json_variant_is_null(variant)) {
+ *storage = _USER_STORAGE_INVALID;
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ k = user_storage_from_string(json_variant_string(variant));
+ if (k < 0)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Storage type '%s' not known.", json_variant_string(variant));
+
+ *storage = k;
+ return 0;
+}
+
+static int json_dispatch_disk_size(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ uint64_t *size = userdata;
+ uintmax_t k;
+
+ if (json_variant_is_null(variant)) {
+ *size = UINT64_MAX;
+ return 0;
+ }
+
+ if (!json_variant_is_unsigned(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an integer.", strna(name));
+
+ k = json_variant_unsigned(variant);
+ if (k < USER_DISK_SIZE_MIN || k > USER_DISK_SIZE_MAX)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "JSON field '%s' is not in valid range %" PRIu64 "…%" PRIu64 ".", strna(name), USER_DISK_SIZE_MIN, USER_DISK_SIZE_MAX);
+
+ *size = k;
+ return 0;
+}
+
+static int json_dispatch_tasks_or_memory_max(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ uint64_t *limit = userdata;
+ uintmax_t k;
+
+ if (json_variant_is_null(variant)) {
+ *limit = UINT64_MAX;
+ return 0;
+ }
+
+ if (!json_variant_is_unsigned(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a integer.", strna(name));
+
+ k = json_variant_unsigned(variant);
+ if (k <= 0 || k >= UINT64_MAX)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "JSON field '%s' is not in valid range %" PRIu64 "…%" PRIu64 ".", strna(name), (uint64_t) 1, UINT64_MAX-1);
+
+ *limit = k;
+ return 0;
+}
+
+static int json_dispatch_weight(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ uint64_t *weight = userdata;
+ uintmax_t k;
+
+ if (json_variant_is_null(variant)) {
+ *weight = UINT64_MAX;
+ return 0;
+ }
+
+ if (!json_variant_is_unsigned(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a integer.", strna(name));
+
+ k = json_variant_unsigned(variant);
+ if (k <= CGROUP_WEIGHT_MIN || k >= CGROUP_WEIGHT_MAX)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "JSON field '%s' is not in valid range %" PRIu64 "…%" PRIu64 ".", strna(name), (uint64_t) CGROUP_WEIGHT_MIN, (uint64_t) CGROUP_WEIGHT_MAX);
+
+ *weight = k;
+ return 0;
+}
+
+int json_dispatch_user_group_list(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ _cleanup_strv_free_ char **l = NULL;
+ char ***list = userdata;
+ JsonVariant *e;
+ int r;
+
+ if (!json_variant_is_array(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of strings.", strna(name));
+
+ JSON_VARIANT_ARRAY_FOREACH(e, variant) {
+
+ if (!json_variant_is_string(e))
+ return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not a string.");
+
+ if (!valid_user_group_name(json_variant_string(e)))
+ return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not a valid user/group name: %s", json_variant_string(e));
+
+ r = strv_extend(&l, json_variant_string(e));
+ if (r < 0)
+ return json_log(e, flags, r, "Failed to append array element: %m");
+ }
+
+ r = strv_extend_strv(list, l, true);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to merge user/group arrays: %m");
+
+ return 0;
+}
+
+static int dispatch_secret(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+
+ static const JsonDispatch secret_dispatch_table[] = {
+ { "password", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, password), 0 },
+ { "pkcs11Pin", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, pkcs11_pin), 0 },
+ { "pkcs11ProtectedAuthenticationPathPermitted", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, pkcs11_protected_authentication_path_permitted), 0 },
+ {},
+ };
+
+ return json_dispatch(variant, secret_dispatch_table, NULL, flags, userdata);
+}
+
+static int dispatch_pkcs11_uri(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ char **s = userdata;
+ const char *n;
+ int r;
+
+ if (json_variant_is_null(variant)) {
+ *s = mfree(*s);
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ n = json_variant_string(variant);
+ if (!pkcs11_uri_valid(n))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid RFC7512 PKCS#11 URI.", strna(name));
+
+ r = free_and_strdup(s, n);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to allocate string: %m");
+
+ return 0;
+}
+
+static int dispatch_pkcs11_uri_array(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ _cleanup_strv_free_ char **z = NULL;
+ char ***l = userdata;
+ JsonVariant *e;
+ int r;
+
+ if (json_variant_is_null(variant)) {
+ *l = strv_free(*l);
+ return 0;
+ }
+
+ if (json_variant_is_string(variant)) {
+ const char *n;
+
+ n = json_variant_string(variant);
+ if (!pkcs11_uri_valid(n))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid RFC7512 PKCS#11 URI.", strna(name));
+
+ z = strv_new(n);
+ if (!z)
+ return log_oom();
+
+ } else {
+
+ if (!json_variant_is_array(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string or array of strings.", strna(name));
+
+ JSON_VARIANT_ARRAY_FOREACH(e, variant) {
+ const char *n;
+
+ if (!json_variant_is_string(e))
+ return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not a string.");
+
+ n = json_variant_string(e);
+ if (!pkcs11_uri_valid(n))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element in '%s' is not a valid RFC7512 PKCS#11 URI: %s", strna(name), n);
+
+ r = strv_extend(&z, n);
+ if (r < 0)
+ return log_oom();
+ }
+ }
+
+ strv_free_and_replace(*l, z);
+ return 0;
+}
+
+static int dispatch_pkcs11_key_data(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ Pkcs11EncryptedKey *k = userdata;
+ size_t l;
+ void *b;
+ int r;
+
+ if (json_variant_is_null(variant)) {
+ k->data = mfree(k->data);
+ k->size = 0;
+ return 0;
+ }
+
+ if (!json_variant_is_string(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
+
+ r = unbase64mem(json_variant_string(variant), (size_t) -1, &b, &l);
+ if (r < 0)
+ return json_log(variant, flags, r, "Failed to decode encrypted PKCS#11 key: %m");
+
+ erase_and_free(k->data);
+ k->data = b;
+ k->size = l;
+
+ return 0;
+}
+
+static int dispatch_pkcs11_key(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ UserRecord *h = userdata;
+ JsonVariant *e;
+ int r;
+
+ if (!json_variant_is_array(variant))
+ return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of objects.", strna(name));
+
+ JSON_VARIANT_ARRAY_FOREACH(e, variant) {
+ Pkcs11EncryptedKey *array, *k;
+
+ static const JsonDispatch pkcs11_key_dispatch_table[] = {
+ { "uri", JSON_VARIANT_STRING, dispatch_pkcs11_uri, offsetof(Pkcs11EncryptedKey, uri), JSON_MANDATORY },
+ { "data", JSON_VARIANT_STRING, dispatch_pkcs11_key_data, 0, JSON_MANDATORY },
+ { "hashedPassword", JSON_VARIANT_STRING, json_dispatch_string, offsetof(Pkcs11EncryptedKey, hashed_password), JSON_MANDATORY },
+ {},
+ };
+
+ if (!json_variant_is_object(e))
+ return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not an object.");
+
+ array = reallocarray(h->pkcs11_encrypted_key, h->n_pkcs11_encrypted_key + 1, sizeof(Pkcs11EncryptedKey));
+ if (!array)
+ return log_oom();
+
+ h->pkcs11_encrypted_key = array;
+ k = h->pkcs11_encrypted_key + h->n_pkcs11_encrypted_key;
+ *k = (Pkcs11EncryptedKey) {};
+
+ r = json_dispatch(e, pkcs11_key_dispatch_table, NULL, flags, k);
+ if (r < 0) {
+ pkcs11_encrypted_key_done(k);
+ return r;
+ }
+
+ h->n_pkcs11_encrypted_key++;
+ }
+
+ return 0;
+}
+
+static int dispatch_privileged(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+
+ static const JsonDispatch privileged_dispatch_table[] = {
+ { "passwordHint", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, password_hint), 0 },
+ { "hashedPassword", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, hashed_password), JSON_SAFE },
+ { "sshAuthorizedKeys", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, ssh_authorized_keys), 0 },
+ { "pkcs11EncryptedKey", JSON_VARIANT_ARRAY, dispatch_pkcs11_key, 0, 0 },
+ {},
+ };
+
+ 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[] = {
+ { "imagePath", JSON_VARIANT_STRING, json_dispatch_image_path, offsetof(UserRecord, image_path), 0 },
+ { "homeDirectory", JSON_VARIANT_STRING, json_dispatch_home_directory, offsetof(UserRecord, home_directory), 0 },
+ { "partitionUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 },
+ { "luksUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 },
+ { "fileSystemUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, file_system_uuid), 0 },
+ { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 },
+ { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 },
+ { "storage", JSON_VARIANT_STRING, json_dispatch_storage, offsetof(UserRecord, storage), 0 },
+ { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE },
+ { "luksCipher", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher), JSON_SAFE },
+ { "luksCipherMode", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher_mode), JSON_SAFE },
+ { "luksVolumeKeySize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_volume_key_size), 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);
+}
+
+int per_machine_id_match(JsonVariant *ids, JsonDispatchFlags flags) {
+ sd_id128_t mid;
+ int r;
+
+ r = sd_id128_get_machine(&mid);
+ if (r < 0)
+ return json_log(ids, flags, r, "Failed to acquire machine ID: %m");
+
+ if (json_variant_is_string(ids)) {
+ sd_id128_t k;
+
+ r = sd_id128_from_string(json_variant_string(ids), &k);
+ if (r < 0) {
+ json_log(ids, flags, r, "%s is not a valid machine ID, ignoring: %m", json_variant_string(ids));
+ return 0;
+ }
+
+ return sd_id128_equal(mid, k);
+ }
+
+ if (json_variant_is_array(ids)) {
+ JsonVariant *e;
+
+ JSON_VARIANT_ARRAY_FOREACH(e, ids) {
+ sd_id128_t k;
+
+ if (!json_variant_is_string(e)) {
+ json_log(e, flags, 0, "Machine ID is not a string, ignoring: %m");
+ continue;
+ }
+
+ r = sd_id128_from_string(json_variant_string(e), &k);
+ if (r < 0) {
+ json_log(e, flags, r, "%s is not a valid machine ID, ignoring: %m", json_variant_string(e));
+ continue;
+ }
+
+ if (sd_id128_equal(mid, k))
+ return true;
+ }
+
+ return false;
+ }
+
+ json_log(ids, flags, 0, "Machine ID is not a string or array of strings, ignoring: %m");
+ return false;
+}
+
+int per_machine_hostname_match(JsonVariant *hns, JsonDispatchFlags flags) {
+ _cleanup_free_ char *hn = NULL;
+ int r;
+
+ r = gethostname_strict(&hn);
+ if (r == -ENXIO) {
+ json_log(hns, flags, r, "No hostname set, not matching perMachine hostname record: %m");
+ return false;
+ }
+ if (r < 0)
+ return json_log(hns, flags, r, "Failed to acquire hostname: %m");
+
+ if (json_variant_is_string(hns))
+ return streq(json_variant_string(hns), hn);
+
+ if (json_variant_is_array(hns)) {
+ JsonVariant *e;
+
+ JSON_VARIANT_ARRAY_FOREACH(e, hns) {
+
+ if (!json_variant_is_string(e)) {
+ json_log(e, flags, 0, "Hostname is not a string, ignoring: %m");
+ continue;
+ }
+
+ if (streq(json_variant_string(hns), hn))
+ return true;
+ }
+
+ return false;
+ }
+
+ json_log(hns, flags, 0, "Hostname is not a string or array of strings, ignoring: %m");
+ return false;
+}
+
+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 },
+ { "iconName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, icon_name), JSON_SAFE },
+ { "location", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, location), 0 },
+ { "shell", JSON_VARIANT_STRING, json_dispatch_filename_or_path, offsetof(UserRecord, shell), 0 },
+ { "umask", JSON_VARIANT_UNSIGNED, json_dispatch_umask, offsetof(UserRecord, umask), 0 },
+ { "environment", JSON_VARIANT_ARRAY, json_dispatch_environment, offsetof(UserRecord, environment), 0 },
+ { "timeZone", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, time_zone), JSON_SAFE },
+ { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, preferred_language), JSON_SAFE },
+ { "niceLevel", _JSON_VARIANT_TYPE_INVALID, json_dispatch_nice, offsetof(UserRecord, nice_level), 0 },
+ { "resourceLimits", _JSON_VARIANT_TYPE_INVALID, json_dispatch_rlimits, offsetof(UserRecord, rlimits), 0 },
+ { "locked", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, locked), 0 },
+ { "notBeforeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_before_usec), 0 },
+ { "notAfterUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_after_usec), 0 },
+ { "storage", JSON_VARIANT_STRING, json_dispatch_storage, offsetof(UserRecord, storage), 0 },
+ { "diskSize", JSON_VARIANT_UNSIGNED, json_dispatch_disk_size, offsetof(UserRecord, disk_size), 0 },
+ { "diskSizeRelative", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_size_relative), 0 },
+ { "skeletonDirectory", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, skeleton_directory), 0 },
+ { "accessMode", JSON_VARIANT_UNSIGNED, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 },
+ { "tasksMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, tasks_max), 0 },
+ { "memoryHigh", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_high), 0 },
+ { "memoryMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_max), 0 },
+ { "cpuWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, cpu_weight), 0 },
+ { "ioWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, io_weight), 0 },
+ { "mountNoDevices", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nodev), 0 },
+ { "mountNoSuid", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nosuid), 0 },
+ { "mountNoExecute", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, noexec), 0 },
+ { "cifsDomain", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_domain), JSON_SAFE },
+ { "cifsUserName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_user_name), JSON_SAFE },
+ { "cifsService", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_service), JSON_SAFE },
+ { "imagePath", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, image_path), 0 },
+ { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 },
+ { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 },
+ { "memberOf", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(UserRecord, member_of), 0 },
+ { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE },
+ { "partitionUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 },
+ { "luksUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 },
+ { "fileSystemUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, file_system_uuid), 0 },
+ { "luksDiscard", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, luks_discard), 0, },
+ { "luksCipher", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher), JSON_SAFE },
+ { "luksCipherMode", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher_mode), JSON_SAFE },
+ { "luksVolumeKeySize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_volume_key_size), 0 },
+ { "luksPbkdfHashAlgorithm", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_hash_algorithm), JSON_SAFE },
+ { "luksPbkdfType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_type), JSON_SAFE },
+ { "luksPbkdfTimeCostUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_time_cost_usec), 0 },
+ { "luksPbkdfMemoryCost", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_memory_cost), 0 },
+ { "luksPbkdfParallelThreads", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_parallel_threads), 0 },
+ { "rateLimitIntervalUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_interval_usec), 0 },
+ { "rateLimitBurst", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_burst), 0 },
+ { "enforcePasswordPolicy", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, enforce_password_policy), 0 },
+ { "autoLogin", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, auto_login), 0 },
+ { "stopDelayUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, stop_delay_usec), 0 },
+ { "killProcesses", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, kill_processes), 0 },
+ { "passwordChangeMinUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_min_usec), 0 },
+ { "passwordChangeMaxUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_max_usec), 0 },
+ { "passwordChangeWarnUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_warn_usec), 0 },
+ { "passwordChangeInactiveUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_inactive_usec), 0 },
+ { "passwordChangeNow", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, password_change_now), 0 },
+ { "pkcs11TokenUri", JSON_VARIANT_ARRAY, dispatch_pkcs11_uri_array, offsetof(UserRecord, pkcs11_token_uri), 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[] = {
+ { "diskUsage", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_usage), 0 },
+ { "diskFree", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_free), 0 },
+ { "diskSize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_size), 0 },
+ { "diskCeiling", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_ceiling), 0 },
+ { "diskFloor", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_floor), 0 },
+ { "state", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, state), JSON_SAFE },
+ { "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, service), JSON_SAFE },
+ { "signedLocally", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, signed_locally), 0 },
+ { "goodAuthenticationCounter", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, good_authentication_counter), 0 },
+ { "badAuthenticationCounter", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, bad_authentication_counter), 0 },
+ { "lastGoodAuthenticationUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_good_authentication_usec), 0 },
+ { "lastBadAuthenticationUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_bad_authentication_usec), 0 },
+ { "rateLimitBeginUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_begin_usec), 0 },
+ { "rateLimitCount", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_count), 0 },
+ { "removable", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, removable), 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, status_dispatch_table, NULL, flags, userdata);
+}
+
+static int user_record_augment(UserRecord *h, JsonDispatchFlags json_flags) {
+ assert(h);
+
+ if (!FLAGS_SET(h->mask, USER_RECORD_REGULAR))
+ return 0;
+
+ assert(h->user_name);
+
+ if (!h->user_name_and_realm_auto && h->realm) {
+ h->user_name_and_realm_auto = strjoin(h->user_name, "@", h->realm);
+ if (!h->user_name_and_realm_auto)
+ return json_log_oom(h->json, json_flags);
+ }
+
+ /* Let's add in the following automatisms only for regular users, they dont make sense for any others */
+ if (user_record_disposition(h) != USER_REGULAR)
+ return 0;
+
+ if (!h->home_directory && !h->home_directory_auto) {
+ h->home_directory_auto = path_join("/home/", h->user_name);
+ if (!h->home_directory_auto)
+ return json_log_oom(h->json, json_flags);
+ }
+
+ if (!h->image_path && !h->image_path_auto) {
+ const char *suffix;
+ UserStorage storage;
+
+ storage = user_record_storage(h);
+ if (storage == USER_LUKS)
+ suffix = ".home";
+ else if (IN_SET(storage, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT))
+ suffix = ".homedir";
+ else
+ suffix = NULL;
+
+ if (suffix) {
+ h->image_path_auto = strjoin("/home/", user_record_user_name_and_realm(h), suffix);
+ if (!h->image_path_auto)
+ return json_log_oom(h->json, json_flags);
+ }
+ }
+
+ return 0;
+}
+
+int user_group_record_mangle(
+ JsonVariant *v,
+ UserRecordLoadFlags load_flags,
+ JsonVariant **ret_variant,
+ UserRecordMask *ret_mask) {
+
+ static const struct {
+ UserRecordMask mask;
+ const char *name;
+ } mask_field[] = {
+ { USER_RECORD_PRIVILEGED, "privileged" },
+ { USER_RECORD_SECRET, "secret" },
+ { USER_RECORD_BINDING, "binding" },
+ { USER_RECORD_PER_MACHINE, "perMachine" },
+ { USER_RECORD_STATUS, "status" },
+ { USER_RECORD_SIGNATURE, "signature" },
+ };
+
+ JsonDispatchFlags json_flags = USER_RECORD_LOAD_FLAGS_TO_JSON_DISPATCH_FLAGS(load_flags);
+ _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
+ JsonVariant *array[ELEMENTSOF(mask_field) * 2];
+ size_t n_retain = 0, i;
+ UserRecordMask m = 0;
+ int r;
+
+ assert((load_flags & _USER_RECORD_MASK_MAX) == 0); /* detect mistakes when accidentally passing
+ * UserRecordMask bit masks as UserRecordLoadFlags
+ * value */
+
+ assert(v);
+ assert(ret_variant);
+ assert(ret_mask);
+
+ /* Note that this function is shared with the group record parser, hence we try to be generic in our
+ * log message wording here, to cover both cases. */
+
+ if (!json_variant_is_object(v))
+ return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record is not a JSON object, refusing.");
+
+ if (USER_RECORD_ALLOW_MASK(load_flags) == 0) /* allow nothing? */
+ return json_log(v, json_flags, SYNTHETIC_ERRNO(EINVAL), "Nothing allowed in record, refusing.");
+
+ if (USER_RECORD_STRIP_MASK(load_flags) == _USER_RECORD_MASK_MAX) /* strip everything? */
+ return json_log(v, json_flags, SYNTHETIC_ERRNO(EINVAL), "Stripping everything from record, refusing.");
+
+ /* Check if we have the special sections and if they match our flags set */
+ for (i = 0; i < ELEMENTSOF(mask_field); i++) {
+ JsonVariant *e, *k;
+
+ if (FLAGS_SET(USER_RECORD_STRIP_MASK(load_flags), mask_field[i].mask)) {
+ if (!w)
+ w = json_variant_ref(v);
+
+ r = json_variant_filter(&w, STRV_MAKE(mask_field[i].name));
+ if (r < 0)
+ return json_log(w, json_flags, r, "Failed to remove field from variant: %m");
+
+ continue;
+ }
+
+ e = json_variant_by_key_full(v, mask_field[i].name, &k);
+ if (e) {
+ if (!FLAGS_SET(USER_RECORD_ALLOW_MASK(load_flags), mask_field[i].mask))
+ return json_log(e, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record contains '%s' field, which is not allowed.", mask_field[i].name);
+
+ if (FLAGS_SET(load_flags, USER_RECORD_STRIP_REGULAR)) {
+ array[n_retain++] = k;
+ array[n_retain++] = e;
+ }
+
+ m |= mask_field[i].mask;
+ } else {
+ if (FLAGS_SET(USER_RECORD_REQUIRE_MASK(load_flags), mask_field[i].mask))
+ return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record lacks '%s' field, which is required.", mask_field[i].name);
+ }
+ }
+
+ if (FLAGS_SET(load_flags, USER_RECORD_STRIP_REGULAR)) {
+ /* If we are supposed to strip regular items, then let's instead just allocate a new object
+ * with just the stuff we need. */
+
+ w = json_variant_unref(w);
+ r = json_variant_new_object(&w, array, n_retain);
+ if (r < 0)
+ return json_log(v, json_flags, r, "Failed to allocate new object: %m");
+ } else {
+ /* And now check if there's anything else in the record */
+ for (i = 0; i < json_variant_elements(v); i += 2) {
+ const char *f;
+ bool special = false;
+ size_t j;
+
+ assert_se(f = json_variant_string(json_variant_by_index(v, i)));
+
+ for (j = 0; j < ELEMENTSOF(mask_field); j++)
+ if (streq(f, mask_field[j].name)) { /* already covered in the loop above */
+ special = true;
+ continue;
+ }
+
+ if (!special) {
+ if ((load_flags & (USER_RECORD_ALLOW_REGULAR|USER_RECORD_REQUIRE_REGULAR)) == 0)
+ return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record contains '%s' field, which is not allowed.", f);
+
+ m |= USER_RECORD_REGULAR;
+ break;
+ }
+ }
+ }
+
+ if (FLAGS_SET(load_flags, USER_RECORD_REQUIRE_REGULAR) && !FLAGS_SET(m, USER_RECORD_REGULAR))
+ return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record lacks basic identity fields, which are required.");
+
+ if (m == 0)
+ return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record is empty.");
+
+ if (w)
+ *ret_variant = TAKE_PTR(w);
+ else
+ *ret_variant = json_variant_ref(v);
+
+ *ret_mask = m;
+ return 0;
+}
+
+int user_record_load(UserRecord *h, JsonVariant *v, UserRecordLoadFlags load_flags) {
+
+ static const JsonDispatch user_dispatch_table[] = {
+ { "userName", JSON_VARIANT_STRING, json_dispatch_user_group_name, offsetof(UserRecord, user_name), 0 },
+ { "realm", JSON_VARIANT_STRING, json_dispatch_realm, offsetof(UserRecord, realm), 0 },
+ { "realName", JSON_VARIANT_STRING, json_dispatch_gecos, offsetof(UserRecord, real_name), 0 },
+ { "emailAddress", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, email_address), JSON_SAFE },
+ { "iconName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, icon_name), JSON_SAFE },
+ { "location", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, location), 0 },
+ { "disposition", JSON_VARIANT_STRING, json_dispatch_user_disposition, offsetof(UserRecord, disposition), 0 },
+ { "lastChangeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_change_usec), 0 },
+ { "lastPasswordChangeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_password_change_usec), 0 },
+ { "shell", JSON_VARIANT_STRING, json_dispatch_filename_or_path, offsetof(UserRecord, shell), 0 },
+ { "umask", JSON_VARIANT_UNSIGNED, json_dispatch_umask, offsetof(UserRecord, umask), 0 },
+ { "environment", JSON_VARIANT_ARRAY, json_dispatch_environment, offsetof(UserRecord, environment), 0 },
+ { "timeZone", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, time_zone), JSON_SAFE },
+ { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, preferred_language), JSON_SAFE },
+ { "niceLevel", _JSON_VARIANT_TYPE_INVALID, json_dispatch_nice, offsetof(UserRecord, nice_level), 0 },
+ { "resourceLimits", _JSON_VARIANT_TYPE_INVALID, json_dispatch_rlimits, offsetof(UserRecord, rlimits), 0 },
+ { "locked", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, locked), 0 },
+ { "notBeforeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_before_usec), 0 },
+ { "notAfterUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_after_usec), 0 },
+ { "storage", JSON_VARIANT_STRING, json_dispatch_storage, offsetof(UserRecord, storage), 0 },
+ { "diskSize", JSON_VARIANT_UNSIGNED, json_dispatch_disk_size, offsetof(UserRecord, disk_size), 0 },
+ { "diskSizeRelative", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_size_relative), 0 },
+ { "skeletonDirectory", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, skeleton_directory), 0 },
+ { "accessMode", JSON_VARIANT_UNSIGNED, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 },
+ { "tasksMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, tasks_max), 0 },
+ { "memoryHigh", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_high), 0 },
+ { "memoryMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_max), 0 },
+ { "cpuWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, cpu_weight), 0 },
+ { "ioWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, io_weight), 0 },
+ { "mountNoDevices", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nodev), 0 },
+ { "mountNoSuid", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nosuid), 0 },
+ { "mountNoExecute", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, noexec), 0 },
+ { "cifsDomain", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_domain), JSON_SAFE },
+ { "cifsUserName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_user_name), JSON_SAFE },
+ { "cifsService", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_service), JSON_SAFE },
+ { "imagePath", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, image_path), 0 },
+ { "homeDirectory", JSON_VARIANT_STRING, json_dispatch_home_directory, offsetof(UserRecord, home_directory), 0 },
+ { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 },
+ { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 },
+ { "memberOf", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(UserRecord, member_of), 0 },
+ { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE },
+ { "partitionUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 },
+ { "luksUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 },
+ { "fileSystemUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, file_system_uuid), 0 },
+ { "luksDiscard", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, luks_discard), 0 },
+ { "luksCipher", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher), JSON_SAFE },
+ { "luksCipherMode", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher_mode), JSON_SAFE },
+ { "luksVolumeKeySize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_volume_key_size), 0 },
+ { "luksPbkdfHashAlgorithm", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_hash_algorithm), JSON_SAFE },
+ { "luksPbkdfType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_type), JSON_SAFE },
+ { "luksPbkdfTimeCostUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_time_cost_usec), 0 },
+ { "luksPbkdfMemoryCost", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_memory_cost), 0 },
+ { "luksPbkdfParallelThreads", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_parallel_threads), 0 },
+ { "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, service), JSON_SAFE },
+ { "rateLimitIntervalUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_interval_usec), 0 },
+ { "rateLimitBurst", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_burst), 0 },
+ { "enforcePasswordPolicy", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, enforce_password_policy), 0 },
+ { "autoLogin", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, auto_login), 0 },
+ { "stopDelayUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, stop_delay_usec), 0 },
+ { "killProcesses", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, kill_processes), 0 },
+ { "passwordChangeMinUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_min_usec), 0 },
+ { "passwordChangeMaxUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_max_usec), 0 },
+ { "passwordChangeWarnUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_warn_usec), 0 },
+ { "passwordChangeInactiveUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_inactive_usec), 0 },
+ { "passwordChangeNow", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, password_change_now), 0 },
+ { "pkcs11TokenUri", JSON_VARIANT_ARRAY, dispatch_pkcs11_uri_array, offsetof(UserRecord, pkcs11_token_uri), 0 },
+
+ { "secret", JSON_VARIANT_OBJECT, dispatch_secret, 0, 0 },
+ { "privileged", JSON_VARIANT_OBJECT, dispatch_privileged, 0, 0 },
+
+ /* Ignore the perMachine, binding, 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! */
+
+ r = user_group_record_mangle(v, load_flags, &h->json, &h->mask);
+ if (r < 0)
+ return r;
+
+ r = json_dispatch(h->json, user_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->user_name)
+ return json_log(h->json, json_flags, SYNTHETIC_ERRNO(EINVAL), "User name field missing, refusing.");
+
+ r = user_record_augment(h, json_flags);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int user_record_build(UserRecord **ret, ...) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(user_record_unrefp) UserRecord *u = 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;
+
+ u = user_record_new();
+ if (!u)
+ return -ENOMEM;
+
+ r = user_record_load(u, v, USER_RECORD_LOAD_FULL);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(u);
+ return 0;
+}
+
+const char *user_record_user_name_and_realm(UserRecord *h) {
+ assert(h);
+
+ /* Return the pre-initialized joined string if it is defined */
+ if (h->user_name_and_realm_auto)
+ return h->user_name_and_realm_auto;
+
+ /* If it's not defined then we cannot have a realm */
+ assert(!h->realm);
+ return h->user_name;
+}
+
+UserStorage user_record_storage(UserRecord *h) {
+ assert(h);
+
+ if (h->storage >= 0)
+ return h->storage;
+
+ return USER_CLASSIC;
+}
+
+const char *user_record_file_system_type(UserRecord *h) {
+ assert(h);
+
+ return h->file_system_type ?: "ext4";
+}
+
+const char *user_record_skeleton_directory(UserRecord *h) {
+ assert(h);
+
+ return h->skeleton_directory ?: "/etc/skel";
+}
+
+mode_t user_record_access_mode(UserRecord *h) {
+ assert(h);
+
+ return h->access_mode != (mode_t) -1 ? h->access_mode : 0700;
+}
+
+const char* user_record_home_directory(UserRecord *h) {
+ assert(h);
+
+ if (h->home_directory)
+ return h->home_directory;
+ if (h->home_directory_auto)
+ return h->home_directory_auto;
+
+ /* The root user is special, hence be special about it */
+ if (streq_ptr(h->user_name, "root"))
+ return "/root";
+
+ return "/";
+}
+
+const char *user_record_image_path(UserRecord *h) {
+ assert(h);
+
+ if (h->image_path)
+ return h->image_path;
+ if (h->image_path_auto)
+ return h->image_path_auto;
+
+ return IN_SET(user_record_storage(h), USER_CLASSIC, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT) ? user_record_home_directory(h) : NULL;
+}
+
+const char *user_record_cifs_user_name(UserRecord *h) {
+ assert(h);
+
+ return h->cifs_user_name ?: h->user_name;
+}
+
+unsigned long user_record_mount_flags(UserRecord *h) {
+ assert(h);
+
+ return (h->nosuid ? MS_NOSUID : 0) |
+ (h->noexec ? MS_NOEXEC : 0) |
+ (h->nodev ? MS_NODEV : 0);
+}
+
+const char *user_record_shell(UserRecord *h) {
+ assert(h);
+
+ if (h->shell)
+ return h->shell;
+
+ if (streq_ptr(h->user_name, "root"))
+ return "/bin/sh";
+
+ if (user_record_disposition(h) == USER_REGULAR)
+ return "/bin/bash";
+
+ return NOLOGIN;
+}
+
+const char *user_record_real_name(UserRecord *h) {
+ assert(h);
+
+ return h->real_name ?: h->user_name;
+}
+
+bool user_record_luks_discard(UserRecord *h) {
+ const char *ip;
+
+ assert(h);
+
+ if (h->luks_discard >= 0)
+ return h->luks_discard;
+
+ ip = user_record_image_path(h);
+ if (!ip)
+ return false;
+
+ /* Use discard by default if we are referring to a real block device, but not when operating on a
+ * loopback device. We want to optimize for SSD and flash storage after all, but we should be careful
+ * when storing stuff on top of regular file systems in loopback files as doing discard then would
+ * mean thin provisioning and we should not do that willy-nilly since it means we'll risk EIO later
+ * on should the disk space to back our file systems not be available. */
+
+ return path_startswith(ip, "/dev/");
+}
+
+const char *user_record_luks_cipher(UserRecord *h) {
+ assert(h);
+
+ return h->luks_cipher ?: "aes";
+}
+
+const char *user_record_luks_cipher_mode(UserRecord *h) {
+ assert(h);
+
+ return h->luks_cipher_mode ?: "xts-plain64";
+}
+
+uint64_t user_record_luks_volume_key_size(UserRecord *h) {
+ assert(h);
+
+ /* We return a value here that can be cast without loss into size_t which is what libcrypsetup expects */
+
+ if (h->luks_volume_key_size == UINT64_MAX)
+ return 256 / 8;
+
+ return MIN(h->luks_volume_key_size, SIZE_MAX);
+}
+
+const char* user_record_luks_pbkdf_type(UserRecord *h) {
+ assert(h);
+
+ return h->luks_pbkdf_type ?: "argon2i";
+}
+
+uint64_t user_record_luks_pbkdf_time_cost_usec(UserRecord *h) {
+ assert(h);
+
+ /* Returns a value with ms granularity, since that's what libcryptsetup expects */
+
+ if (h->luks_pbkdf_time_cost_usec == UINT64_MAX)
+ return 500 * USEC_PER_MSEC; /* We default to 500ms, in contrast to libcryptsetup's 2s, which is just awfully slow on every login */
+
+ return MIN(DIV_ROUND_UP(h->luks_pbkdf_time_cost_usec, USEC_PER_MSEC), UINT32_MAX) * USEC_PER_MSEC;
+}
+
+uint64_t user_record_luks_pbkdf_memory_cost(UserRecord *h) {
+ assert(h);
+
+ /* Returns a value with kb granularity, since that's what libcryptsetup expects */
+
+ if (h->luks_pbkdf_memory_cost == UINT64_MAX)
+ return 64*1024*1024; /* We default to 64M, since this should work on smaller systems too */
+
+ return MIN(DIV_ROUND_UP(h->luks_pbkdf_memory_cost, 1024), UINT32_MAX) * 1024;
+}
+
+uint64_t user_record_luks_pbkdf_parallel_threads(UserRecord *h) {
+ assert(h);
+
+ if (h->luks_pbkdf_memory_cost == UINT64_MAX)
+ return 1; /* We default to 1, since this should work on smaller systems too */
+
+ return MIN(h->luks_pbkdf_parallel_threads, UINT32_MAX);
+}
+
+const char *user_record_luks_pbkdf_hash_algorithm(UserRecord *h) {
+ assert(h);
+
+ return h->luks_pbkdf_hash_algorithm ?: "sha512";
+}
+
+gid_t user_record_gid(UserRecord *h) {
+ assert(h);
+
+ if (gid_is_valid(h->gid))
+ return h->gid;
+
+ return (gid_t) h->uid;
+}
+
+UserDisposition user_record_disposition(UserRecord *h) {
+ assert(h);
+
+ if (h->disposition >= 0)
+ return h->disposition;
+
+ /* If not declared, derive from UID */
+
+ if (!uid_is_valid(h->uid))
+ return _USER_DISPOSITION_INVALID;
+
+ if (h->uid == 0 || h->uid == UID_NOBODY)
+ return USER_INTRINSIC;
+
+ if (uid_is_system(h->uid))
+ return USER_SYSTEM;
+
+ if (uid_is_dynamic(h->uid))
+ return USER_DYNAMIC;
+
+ if (uid_is_container(h->uid))
+ return USER_CONTAINER;
+
+ if (h->uid > INT32_MAX)
+ return USER_RESERVED;
+
+ return USER_REGULAR;
+}
+
+int user_record_removable(UserRecord *h) {
+ UserStorage storage;
+ assert(h);
+
+ if (h->removable >= 0)
+ return h->removable;
+
+ /* Refuse to decide for classic records */
+ storage = user_record_storage(h);
+ if (h->storage < 0 || h->storage == USER_CLASSIC)
+ return -1;
+
+ /* For now consider only LUKS home directories with a reference by path as removable */
+ return storage == USER_LUKS && path_startswith(user_record_image_path(h), "/dev/");
+}
+
+uint64_t user_record_ratelimit_interval_usec(UserRecord *h) {
+ assert(h);
+
+ if (h->ratelimit_interval_usec == UINT64_MAX)
+ return DEFAULT_RATELIMIT_INTERVAL_USEC;
+
+ return h->ratelimit_interval_usec;
+}
+
+uint64_t user_record_ratelimit_burst(UserRecord *h) {
+ assert(h);
+
+ if (h->ratelimit_burst == UINT64_MAX)
+ return DEFAULT_RATELIMIT_BURST;
+
+ return h->ratelimit_burst;
+}
+
+bool user_record_can_authenticate(UserRecord *h) {
+ assert(h);
+
+ /* Returns true if there's some form of property configured that the user can authenticate against */
+
+ if (h->n_pkcs11_encrypted_key > 0)
+ return true;
+
+ return !strv_isempty(h->hashed_password);
+}
+
+uint64_t user_record_ratelimit_next_try(UserRecord *h) {
+ assert(h);
+
+ /* Calculates when the it's possible to login next. Returns:
+ *
+ * UINT64_MAX → Nothing known
+ * 0 → Right away
+ * Any other → Next time in CLOCK_REALTIME in usec (which could be in the past)
+ */
+
+ if (h->ratelimit_begin_usec == UINT64_MAX ||
+ h->ratelimit_count == UINT64_MAX)
+ return UINT64_MAX;
+
+ if (h->ratelimit_count < user_record_ratelimit_burst(h))
+ return 0;
+
+ return usec_add(h->ratelimit_begin_usec, user_record_ratelimit_interval_usec(h));
+}
+
+bool user_record_equal(UserRecord *a, UserRecord *b) {
+ assert(a);
+ assert(b);
+
+ /* We assume that when a record is modified its JSON data is updated at the same time, hence it's
+ * sufficient to compare the JSON data. */
+
+ return json_variant_equal(a->json, b->json);
+}
+
+bool user_record_compatible(UserRecord *a, UserRecord *b) {
+ assert(a);
+ assert(b);
+
+ /* If either lacks a the regular section, we can't really decide, let's hence say they are
+ * incompatible. */
+ if (!(a->mask & b->mask & USER_RECORD_REGULAR))
+ return false;
+
+ return streq_ptr(a->user_name, b->user_name) &&
+ streq_ptr(a->realm, b->realm);
+}
+
+int user_record_compare_last_change(UserRecord *a, UserRecord *b) {
+ assert(a);
+ assert(b);
+
+ if (a->last_change_usec == b->last_change_usec)
+ return 0;
+
+ /* Always consider a record with a timestamp newer than one without */
+ if (a->last_change_usec == UINT64_MAX)
+ return -1;
+ if (b->last_change_usec == UINT64_MAX)
+ return 1;
+
+ return CMP(a->last_change_usec, b->last_change_usec);
+}
+
+int user_record_clone(UserRecord *h, UserRecordLoadFlags flags, UserRecord **ret) {
+ _cleanup_(user_record_unrefp) UserRecord *c = NULL;
+ int r;
+
+ assert(h);
+ assert(ret);
+
+ c = user_record_new();
+ if (!c)
+ return -ENOMEM;
+
+ r = user_record_load(c, h->json, flags);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(c);
+ return 0;
+}
+
+int user_record_masked_equal(UserRecord *a, UserRecord *b, UserRecordMask mask) {
+ _cleanup_(user_record_unrefp) UserRecord *x = NULL, *y = NULL;
+ int r;
+
+ assert(a);
+ assert(b);
+
+ /* Compares the two records, but ignores anything not listed in the specified mask */
+
+ if ((a->mask & ~mask) != 0) {
+ r = user_record_clone(a, USER_RECORD_ALLOW(mask) | USER_RECORD_STRIP(~mask & _USER_RECORD_MASK_MAX), &x);
+ if (r < 0)
+ return r;
+
+ a = x;
+ }
+
+ if ((b->mask & ~mask) != 0) {
+ r = user_record_clone(b, USER_RECORD_ALLOW(mask) | USER_RECORD_STRIP(~mask & _USER_RECORD_MASK_MAX), &y);
+ if (r < 0)
+ return r;
+
+ b = y;
+ }
+
+ return user_record_equal(a, b);
+}
+
+int user_record_test_blocked(UserRecord *h) {
+ usec_t n;
+
+ /* Checks whether access to the specified user shall be allowed at the moment. Returns:
+ *
+ * -ESTALE: Record is from the future
+ * -ENOLCK: Record is blocked
+ * -EL2HLT: Record is not valid yet
+ * -EL3HLT: Record is not valid anymore
+ *
+ */
+
+ assert(h);
+
+ n = now(CLOCK_REALTIME);
+ if (h->last_change_usec != UINT64_MAX &&
+ h->last_change_usec > n) /* Don't allow log ins when the record is from the future */
+ return -ESTALE;
+
+ if (h->locked > 0)
+ return -ENOLCK;
+
+ if (h->not_before_usec != UINT64_MAX && n < h->not_before_usec)
+ return -EL2HLT;
+ if (h->not_after_usec != UINT64_MAX && n > h->not_after_usec)
+ return -EL3HLT;
+
+ return 0;
+}
+
+int user_record_test_password_change_required(UserRecord *h) {
+ bool change_permitted;
+ usec_t n;
+
+ assert(h);
+
+ /* Checks whether the user must change the password when logging in
+
+ -EKEYREVOKED: Change password now because admin said so
+ -EOWNERDEAD: Change password now because it expired
+ -EKEYREJECTED: Password is expired, no changing is allowed
+ -EKEYEXPIRED: Password is about to expire, warn user
+ -ENETDOWN: Record has expiration info but no password change timestamp
+ -EROFS: No password change required nor permitted
+ 0: No password change required, but permitted
+ */
+
+ /* If a pasword change request has been set explicitly, it overrides everything */
+ if (h->password_change_now > 0)
+ return -EKEYREVOKED;
+
+ n = now(CLOCK_REALTIME);
+
+ /* Then, let's check if password changing is currently allowed at all */
+ if (h->password_change_min_usec != UINT64_MAX) {
+
+ /* Expiry configured but no password change timestamp known? */
+ if (h->last_password_change_usec == UINT64_MAX)
+ return -ENETDOWN;
+
+ if (h->password_change_min_usec >= UINT64_MAX - h->last_password_change_usec)
+ change_permitted = false;
+ else
+ change_permitted = n >= h->last_password_change_usec + h->password_change_min_usec;
+
+ } else
+ change_permitted = true;
+
+ /* Let's check whether the password has expired. */
+ if (!(h->password_change_max_usec == UINT64_MAX ||
+ h->password_change_max_usec >= UINT64_MAX - h->last_password_change_usec)) {
+
+ uint64_t change_before;
+
+ /* Expiry configured but no password change timestamp known? */
+ if (h->last_password_change_usec == UINT64_MAX)
+ return -ENETDOWN;
+
+ /* Password is in inactive phase? */
+ if (h->password_change_inactive_usec != UINT64_MAX &&
+ h->password_change_inactive_usec < UINT64_MAX - h->password_change_max_usec) {
+ usec_t added;
+
+ added = h->password_change_inactive_usec + h->password_change_max_usec;
+ if (added < UINT64_MAX - h->last_password_change_usec &&
+ n >= h->last_password_change_usec + added)
+ return -EKEYREJECTED;
+ }
+
+ /* Password needs to be changed now? */
+ change_before = h->last_password_change_usec + h->password_change_max_usec;
+ if (n >= change_before)
+ return change_permitted ? -EOWNERDEAD : -EKEYREJECTED;
+
+ /* Warn user? */
+ if (h->password_change_warn_usec != UINT64_MAX &&
+ (change_before < h->password_change_warn_usec ||
+ n >= change_before - h->password_change_warn_usec))
+ return change_permitted ? -EKEYEXPIRED : -EROFS;
+ }
+
+ /* No password changing necessary */
+ return change_permitted ? 0 : -EROFS;
+}
+
+static const char* const user_storage_table[_USER_STORAGE_MAX] = {
+ [USER_CLASSIC] = "classic",
+ [USER_LUKS] = "luks",
+ [USER_DIRECTORY] = "directory",
+ [USER_SUBVOLUME] = "subvolume",
+ [USER_FSCRYPT] = "fscrypt",
+ [USER_CIFS] = "cifs",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(user_storage, UserStorage);
+
+static const char* const user_disposition_table[_USER_DISPOSITION_MAX] = {
+ [USER_INTRINSIC] = "intrinsic",
+ [USER_SYSTEM] = "system",
+ [USER_DYNAMIC] = "dynamic",
+ [USER_REGULAR] = "regular",
+ [USER_CONTAINER] = "container",
+ [USER_RESERVED] = "reserved",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(user_disposition, UserDisposition);
diff --git a/src/shared/user-record.h b/src/shared/user-record.h
new file mode 100644
index 0000000000..5bac304767
--- /dev/null
+++ b/src/shared/user-record.h
@@ -0,0 +1,375 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+#include
+
+#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_;
diff --git a/src/shared/userdb.c b/src/shared/userdb.c
new file mode 100644
index 0000000000..af2a1cc76f
--- /dev/null
+++ b/src/shared/userdb.c
@@ -0,0 +1,1347 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+
+#include "dirent-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "group-record-nss.h"
+#include "missing_syscall.h"
+#include "parse-util.h"
+#include "set.h"
+#include "socket-util.h"
+#include "strv.h"
+#include "user-record-nss.h"
+#include "user-util.h"
+#include "userdb.h"
+#include "varlink.h"
+
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(link_hash_ops, void, trivial_hash_func, trivial_compare_func, Varlink, varlink_unref);
+
+typedef enum LookupWhat {
+ LOOKUP_USER,
+ LOOKUP_GROUP,
+ LOOKUP_MEMBERSHIP,
+ _LOOKUP_WHAT_MAX,
+} LookupWhat;
+
+struct UserDBIterator {
+ LookupWhat what;
+ Set *links;
+ bool nss_covered:1;
+ bool nss_iterating:1;
+ bool synthesize_root:1;
+ bool synthesize_nobody:1;
+ int error;
+ int nss_lock;
+ unsigned n_found;
+ sd_event *event;
+ UserRecord *found_user; /* when .what == LOOKUP_USER */
+ GroupRecord *found_group; /* when .what == LOOKUP_GROUP */
+
+ char *found_user_name, *found_group_name; /* when .what == LOOKUP_MEMBERSHIP */
+ char **members_of_group;
+ size_t index_members_of_group;
+ char *filter_user_name;
+};
+
+UserDBIterator* userdb_iterator_free(UserDBIterator *iterator) {
+ if (!iterator)
+ return NULL;
+
+ set_free(iterator->links);
+
+ switch (iterator->what) {
+
+ case LOOKUP_USER:
+ user_record_unref(iterator->found_user);
+
+ if (iterator->nss_iterating)
+ endpwent();
+
+ break;
+
+ case LOOKUP_GROUP:
+ group_record_unref(iterator->found_group);
+
+ if (iterator->nss_iterating)
+ endgrent();
+
+ break;
+
+ case LOOKUP_MEMBERSHIP:
+ free(iterator->found_user_name);
+ free(iterator->found_group_name);
+ strv_free(iterator->members_of_group);
+ free(iterator->filter_user_name);
+
+ if (iterator->nss_iterating)
+ endgrent();
+
+ break;
+
+ default:
+ assert_not_reached("Unexpected state?");
+ }
+
+ sd_event_unref(iterator->event);
+ safe_close(iterator->nss_lock);
+
+ return mfree(iterator);
+}
+
+static UserDBIterator* userdb_iterator_new(LookupWhat what) {
+ UserDBIterator *i;
+
+ assert(what >= 0);
+ assert(what < _LOOKUP_WHAT_MAX);
+
+ i = new(UserDBIterator, 1);
+ if (!i)
+ return NULL;
+
+ *i = (UserDBIterator) {
+ .what = what,
+ .nss_lock = -1,
+ };
+
+ return i;
+}
+
+struct user_group_data {
+ JsonVariant *record;
+ bool incomplete;
+};
+
+static void user_group_data_release(struct user_group_data *d) {
+ json_variant_unref(d->record);
+}
+
+static int userdb_on_query_reply(
+ Varlink *link,
+ JsonVariant *parameters,
+ const char *error_id,
+ VarlinkReplyFlags flags,
+ void *userdata) {
+
+ UserDBIterator *iterator = userdata;
+ int r;
+
+ assert(iterator);
+
+ if (error_id) {
+ log_debug("Got lookup error: %s", error_id);
+
+ if (STR_IN_SET(error_id,
+ "io.systemd.UserDatabase.NoRecordFound",
+ "io.systemd.UserDatabase.ConflictingRecordFound"))
+ r = -ESRCH;
+ else if (streq(error_id, "io.systemd.UserDatabase.ServiceNotAvailable"))
+ r = -EHOSTDOWN;
+ else if (streq(error_id, VARLINK_ERROR_TIMEOUT))
+ r = -ETIMEDOUT;
+ else
+ r = -EIO;
+
+ goto finish;
+ }
+
+ switch (iterator->what) {
+
+ case LOOKUP_USER: {
+ _cleanup_(user_group_data_release) struct user_group_data user_data = {};
+
+ static const JsonDispatch dispatch_table[] = {
+ { "record", _JSON_VARIANT_TYPE_INVALID, json_dispatch_variant, offsetof(struct user_group_data, record), 0 },
+ { "incomplete", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(struct user_group_data, incomplete), 0 },
+ {}
+ };
+ _cleanup_(user_record_unrefp) UserRecord *hr = NULL;
+
+ assert_se(!iterator->found_user);
+
+ r = json_dispatch(parameters, dispatch_table, NULL, 0, &user_data);
+ if (r < 0)
+ goto finish;
+
+ if (!user_data.record) {
+ r = log_debug_errno(SYNTHETIC_ERRNO(EIO), "Reply is missing record key");
+ goto finish;
+ }
+
+ hr = user_record_new();
+ if (!hr) {
+ r = -ENOMEM;
+ goto finish;
+ }
+
+ r = user_record_load(hr, user_data.record, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_PERMISSIVE);
+ if (r < 0)
+ goto finish;
+
+ if (!hr->service) {
+ r = log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "User record does not carry service information, refusing.");
+ goto finish;
+ }
+
+ hr->incomplete = user_data.incomplete;
+
+ /* We match the root user by the name since the name is our primary key. We match the nobody
+ * use by UID though, since the name might differ on OSes */
+ if (streq_ptr(hr->user_name, "root"))
+ iterator->synthesize_root = false;
+ if (hr->uid == UID_NOBODY)
+ iterator->synthesize_nobody = false;
+
+ iterator->found_user = TAKE_PTR(hr);
+ iterator->n_found++;
+
+ /* More stuff coming? then let's just exit cleanly here */
+ if (FLAGS_SET(flags, VARLINK_REPLY_CONTINUES))
+ return 0;
+
+ /* Otherwise, let's remove this link and exit cleanly then */
+ r = 0;
+ goto finish;
+ }
+
+ case LOOKUP_GROUP: {
+ _cleanup_(user_group_data_release) struct user_group_data group_data = {};
+
+ static const JsonDispatch dispatch_table[] = {
+ { "record", _JSON_VARIANT_TYPE_INVALID, json_dispatch_variant, offsetof(struct user_group_data, record), 0 },
+ { "incomplete", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(struct user_group_data, incomplete), 0 },
+ {}
+ };
+ _cleanup_(group_record_unrefp) GroupRecord *g = NULL;
+
+ assert_se(!iterator->found_group);
+
+ r = json_dispatch(parameters, dispatch_table, NULL, 0, &group_data);
+ if (r < 0)
+ goto finish;
+
+ if (!group_data.record) {
+ r = log_debug_errno(SYNTHETIC_ERRNO(EIO), "Reply is missing record key");
+ goto finish;
+ }
+
+ g = group_record_new();
+ if (!g) {
+ r = -ENOMEM;
+ goto finish;
+ }
+
+ r = group_record_load(g, group_data.record, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_PERMISSIVE);
+ if (r < 0)
+ goto finish;
+
+ if (!g->service) {
+ r = log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Group record does not carry service information, refusing.");
+ goto finish;
+ }
+
+ g->incomplete = group_data.incomplete;
+
+ if (streq_ptr(g->group_name, "root"))
+ iterator->synthesize_root = false;
+ if (g->gid == GID_NOBODY)
+ iterator->synthesize_nobody = false;
+
+ iterator->found_group = TAKE_PTR(g);
+ iterator->n_found++;
+
+ if (FLAGS_SET(flags, VARLINK_REPLY_CONTINUES))
+ return 0;
+
+ r = 0;
+ goto finish;
+ }
+
+ case LOOKUP_MEMBERSHIP: {
+ struct membership_data {
+ const char *user_name;
+ const char *group_name;
+ } membership_data = {};
+
+ static const JsonDispatch dispatch_table[] = {
+ { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(struct membership_data, user_name), JSON_SAFE },
+ { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(struct membership_data, group_name), JSON_SAFE },
+ {}
+ };
+
+ assert(!iterator->found_user_name);
+ assert(!iterator->found_group_name);
+
+ r = json_dispatch(parameters, dispatch_table, NULL, 0, &membership_data);
+ if (r < 0)
+ goto finish;
+
+ iterator->found_user_name = mfree(iterator->found_user_name);
+ iterator->found_group_name = mfree(iterator->found_group_name);
+
+ iterator->found_user_name = strdup(membership_data.user_name);
+ if (!iterator->found_user_name) {
+ r = -ENOMEM;
+ goto finish;
+ }
+
+ iterator->found_group_name = strdup(membership_data.group_name);
+ if (!iterator->found_group_name) {
+ r = -ENOMEM;
+ goto finish;
+ }
+
+ iterator->n_found++;
+
+ if (FLAGS_SET(flags, VARLINK_REPLY_CONTINUES))
+ return 0;
+
+ r = 0;
+ goto finish;
+ }
+
+ default:
+ assert_not_reached("unexpected lookup");
+ }
+
+finish:
+ /* If we got one ESRCH, let that win. This way when we do a wild dump we won't be tripped up by bad
+ * errors if at least one connection ended cleanly */
+ if (r == -ESRCH || iterator->error == 0)
+ iterator->error = -r;
+
+ assert_se(set_remove(iterator->links, link) == link);
+ link = varlink_unref(link);
+ return 0;
+}
+
+static int userdb_connect(
+ UserDBIterator *iterator,
+ const char *path,
+ const char *method,
+ bool more,
+ JsonVariant *query) {
+
+ _cleanup_(varlink_unrefp) Varlink *vl = NULL;
+ int r;
+
+ assert(iterator);
+ assert(path);
+ assert(method);
+
+ r = varlink_connect_address(&vl, path);
+ if (r < 0)
+ return log_debug_errno(r, "Unable to connect to %s: %m", path);
+
+ varlink_set_userdata(vl, iterator);
+
+ if (!iterator->event) {
+ r = sd_event_new(&iterator->event);
+ if (r < 0)
+ return log_debug_errno(r, "Unable to allocate event loop: %m");
+ }
+
+ r = varlink_attach_event(vl, iterator->event, SD_EVENT_PRIORITY_NORMAL);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to attach varlink connection to event loop: %m");
+
+ (void) varlink_set_description(vl, path);
+
+ r = varlink_bind_reply(vl, userdb_on_query_reply);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to bind reply callback: %m");
+
+ if (more)
+ r = varlink_observe(vl, method, query);
+ else
+ r = varlink_invoke(vl, method, query);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to invoke varlink method: %m");
+
+ r = set_ensure_allocated(&iterator->links, &link_hash_ops);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to allocate set: %m");
+
+ r = set_put(iterator->links, vl);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to add varlink connection to set: %m");
+
+ TAKE_PTR(vl);
+ return r;
+}
+
+static int userdb_start_query(
+ UserDBIterator *iterator,
+ const char *method,
+ bool more,
+ JsonVariant *query,
+ UserDBFlags flags) {
+
+ _cleanup_(strv_freep) char **except = NULL, **only = NULL;
+ _cleanup_(closedirp) DIR *d = NULL;
+ struct dirent *de;
+ const char *e;
+ int r, ret = 0;
+
+ assert(iterator);
+ assert(method);
+
+ e = getenv("SYSTEMD_BYPASS_USERDB");
+ if (e) {
+ r = parse_boolean(e);
+ if (r > 0)
+ return -ENOLINK;
+ if (r < 0) {
+ except = strv_split(e, ":");
+ if (!except)
+ return -ENOMEM;
+ }
+ }
+
+ e = getenv("SYSTEMD_ONLY_USERDB");
+ if (e) {
+ only = strv_split(e, ":");
+ if (!only)
+ return -ENOMEM;
+ }
+
+ /* First, let's talk to the multiplexer, if we can */
+ if ((flags & (USERDB_AVOID_MULTIPLEXER|USERDB_AVOID_DYNAMIC_USER|USERDB_AVOID_NSS|USERDB_DONT_SYNTHESIZE)) == 0 &&
+ !strv_contains(except, "io.systemd.Multiplexer") &&
+ (!only || strv_contains(only, "io.systemd.Multiplexer"))) {
+ _cleanup_(json_variant_unrefp) JsonVariant *patched_query = json_variant_ref(query);
+
+ r = json_variant_set_field_string(&patched_query, "service", "io.systemd.Multiplexer");
+ if (r < 0)
+ return log_debug_errno(r, "Unable to set service JSON field: %m");
+
+ r = userdb_connect(iterator, "/run/systemd/userdb/io.systemd.Multiplexer", method, more, patched_query);
+ if (r >= 0) {
+ iterator->nss_covered = true; /* The multiplexer does NSS */
+ return 0;
+ }
+ }
+
+ d = opendir("/run/systemd/userdb/");
+ if (!d) {
+ if (errno == ENOENT)
+ return -ESRCH;
+
+ return -errno;
+ }
+
+ FOREACH_DIRENT(de, d, return -errno) {
+ _cleanup_(json_variant_unrefp) JsonVariant *patched_query = NULL;
+ _cleanup_free_ char *p = NULL;
+ bool is_nss;
+
+ if (streq(de->d_name, "io.systemd.Multiplexer")) /* We already tried this above, don't try this again */
+ continue;
+
+ if (FLAGS_SET(flags, USERDB_AVOID_DYNAMIC_USER) &&
+ streq(de->d_name, "io.systemd.DynamicUser"))
+ continue;
+
+ /* Avoid NSS is this is requested. Note that we also skip NSS when we were asked to skip the
+ * multiplexer, since in that case it's safer to do NSS in the client side emulation below
+ * (and when we run as part of systemd-userdbd.service we don't want to talk to ourselves
+ * anyway). */
+ is_nss = streq(de->d_name, "io.systemd.NameServiceSwitch");
+ if ((flags & (USERDB_AVOID_NSS|USERDB_AVOID_MULTIPLEXER)) && is_nss)
+ continue;
+
+ if (strv_contains(except, de->d_name))
+ continue;
+
+ if (only && !strv_contains(only, de->d_name))
+ continue;
+
+ p = path_join("/run/systemd/userdb/", de->d_name);
+ if (!p)
+ return -ENOMEM;
+
+ patched_query = json_variant_ref(query);
+ r = json_variant_set_field_string(&patched_query, "service", de->d_name);
+ if (r < 0)
+ return log_debug_errno(r, "Unable to set service JSON field: %m");
+
+ r = userdb_connect(iterator, p, method, more, patched_query);
+ if (is_nss && r >= 0) /* Turn off fallback NSS if we found the NSS service and could connect
+ * to it */
+ iterator->nss_covered = true;
+
+ if (ret == 0 && r < 0)
+ ret = r;
+ }
+
+ if (set_isempty(iterator->links))
+ return ret; /* propagate last error we saw if we couldn't connect to anything. */
+
+ /* We connected to some services, in this case, ignore the ones we failed on */
+ return 0;
+}
+
+static int userdb_process(
+ UserDBIterator *iterator,
+ UserRecord **ret_user_record,
+ GroupRecord **ret_group_record,
+ char **ret_user_name,
+ char **ret_group_name) {
+
+ int r;
+
+ assert(iterator);
+
+ for (;;) {
+ if (iterator->what == LOOKUP_USER && iterator->found_user) {
+ if (ret_user_record)
+ *ret_user_record = TAKE_PTR(iterator->found_user);
+ else
+ iterator->found_user = user_record_unref(iterator->found_user);
+
+ if (ret_group_record)
+ *ret_group_record = NULL;
+ if (ret_user_name)
+ *ret_user_name = NULL;
+ if (ret_group_name)
+ *ret_group_name = NULL;
+
+ return 0;
+ }
+
+ if (iterator->what == LOOKUP_GROUP && iterator->found_group) {
+ if (ret_group_record)
+ *ret_group_record = TAKE_PTR(iterator->found_group);
+ else
+ iterator->found_group = group_record_unref(iterator->found_group);
+
+ if (ret_user_record)
+ *ret_user_record = NULL;
+ if (ret_user_name)
+ *ret_user_name = NULL;
+ if (ret_group_name)
+ *ret_group_name = NULL;
+
+ return 0;
+ }
+
+ if (iterator->what == LOOKUP_MEMBERSHIP && iterator->found_user_name && iterator->found_group_name) {
+ if (ret_user_name)
+ *ret_user_name = TAKE_PTR(iterator->found_user_name);
+ else
+ iterator->found_user_name = mfree(iterator->found_user_name);
+
+ if (ret_group_name)
+ *ret_group_name = TAKE_PTR(iterator->found_group_name);
+ else
+ iterator->found_group_name = mfree(iterator->found_group_name);
+
+ if (ret_user_record)
+ *ret_user_record = NULL;
+ if (ret_group_record)
+ *ret_group_record = NULL;
+
+ return 0;
+ }
+
+ if (set_isempty(iterator->links)) {
+ if (iterator->error == 0)
+ return -ESRCH;
+
+ return -abs(iterator->error);
+ }
+
+ if (!iterator->event)
+ return -ESRCH;
+
+ r = sd_event_run(iterator->event, UINT64_MAX);
+ if (r < 0)
+ return r;
+ }
+}
+
+static int synthetic_root_user_build(UserRecord **ret) {
+ return user_record_build(
+ ret,
+ JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING("root")),
+ JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(0)),
+ JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(0)),
+ JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_STRING("/root")),
+ JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("intrinsic"))));
+}
+
+static int synthetic_nobody_user_build(UserRecord **ret) {
+ return user_record_build(
+ ret,
+ JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(NOBODY_USER_NAME)),
+ JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(UID_NOBODY)),
+ JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(GID_NOBODY)),
+ JSON_BUILD_PAIR("shell", JSON_BUILD_STRING(NOLOGIN)),
+ JSON_BUILD_PAIR("locked", JSON_BUILD_BOOLEAN(true)),
+ JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("intrinsic"))));
+}
+
+int userdb_by_name(const char *name, UserDBFlags flags, UserRecord **ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *query = NULL;
+ int r;
+
+ if (!valid_user_group_name(name))
+ return -EINVAL;
+
+ r = json_build(&query, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(name))));
+ if (r < 0)
+ return r;
+
+ iterator = userdb_iterator_new(LOOKUP_USER);
+ if (!iterator)
+ return -ENOMEM;
+
+ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", false, query, flags);
+ if (r >= 0) {
+ r = userdb_process(iterator, ret, NULL, NULL, NULL);
+ if (r >= 0)
+ return r;
+ }
+
+ if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && !(iterator && iterator->nss_covered)) {
+ /* Make sure the NSS lookup doesn't recurse back to us. (EBUSY is fine here, it just means we
+ * already took the lock from our thread, which is totally OK.) */
+ r = userdb_nss_compat_disable();
+ if (r >= 0 || r == -EBUSY) {
+ iterator->nss_lock = r;
+
+ /* Client-side NSS fallback */
+ r = nss_user_record_by_name(name, ret);
+ if (r >= 0)
+ return r;
+ }
+ }
+
+ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) {
+ if (streq(name, "root"))
+ return synthetic_root_user_build(ret);
+
+ if (streq(name, NOBODY_USER_NAME) && synthesize_nobody())
+ return synthetic_nobody_user_build(ret);
+ }
+
+ return r;
+}
+
+int userdb_by_uid(uid_t uid, UserDBFlags flags, UserRecord **ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *query = NULL;
+ int r;
+
+ if (!uid_is_valid(uid))
+ return -EINVAL;
+
+ r = json_build(&query, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(uid))));
+ if (r < 0)
+ return r;
+
+ iterator = userdb_iterator_new(LOOKUP_USER);
+ if (!iterator)
+ return -ENOMEM;
+
+ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", false, query, flags);
+ if (r >= 0) {
+ r = userdb_process(iterator, ret, NULL, NULL, NULL);
+ if (r >= 0)
+ return r;
+ }
+
+ if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && !(iterator && iterator->nss_covered)) {
+ r = userdb_nss_compat_disable();
+ if (r >= 0 || r == -EBUSY) {
+ iterator->nss_lock = r;
+
+ /* Client-side NSS fallback */
+ r = nss_user_record_by_uid(uid, ret);
+ if (r >= 0)
+ return r;
+ }
+ }
+
+ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) {
+ if (uid == 0)
+ return synthetic_root_user_build(ret);
+
+ if (uid == UID_NOBODY && synthesize_nobody())
+ return synthetic_nobody_user_build(ret);
+ }
+
+ return r;
+}
+
+int userdb_all(UserDBFlags flags, UserDBIterator **ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ int r;
+
+ assert(ret);
+
+ iterator = userdb_iterator_new(LOOKUP_USER);
+ if (!iterator)
+ return -ENOMEM;
+
+ iterator->synthesize_root = iterator->synthesize_nobody = !FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE);
+
+ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", true, NULL, flags);
+
+ if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && (r < 0 || !iterator->nss_covered)) {
+ iterator->nss_lock = userdb_nss_compat_disable();
+ if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY)
+ return iterator->nss_lock;
+
+ setpwent();
+ iterator->nss_iterating = true;
+ goto finish;
+ }
+
+ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE))
+ goto finish;
+
+ return r;
+
+finish:
+ *ret = TAKE_PTR(iterator);
+ return 0;
+}
+
+int userdb_iterator_get(UserDBIterator *iterator, UserRecord **ret) {
+ int r;
+
+ assert(iterator);
+ assert(iterator->what == LOOKUP_USER);
+
+ if (iterator->nss_iterating) {
+ struct passwd *pw;
+
+ /* If NSS isn't covered elsewhere, let's iterate through it first, since it probably contains
+ * the more traditional sources, which are probably good to show first. */
+
+ pw = getpwent();
+ if (pw) {
+ _cleanup_free_ char *buffer = NULL;
+ bool incomplete = false;
+ struct spwd spwd;
+
+ if (streq_ptr(pw->pw_name, "root"))
+ iterator->synthesize_root = false;
+ if (pw->pw_uid == UID_NOBODY)
+ iterator->synthesize_nobody = false;
+
+ r = nss_spwd_for_passwd(pw, &spwd, &buffer);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to acquire shadow entry for user %s, ignoring: %m", pw->pw_name);
+ incomplete = ERRNO_IS_PRIVILEGE(r);
+ }
+
+ r = nss_passwd_to_user_record(pw, r >= 0 ? &spwd : NULL, ret);
+ if (r < 0)
+ return r;
+
+ if (ret)
+ (*ret)->incomplete = incomplete;
+ return r;
+ }
+
+ if (errno != 0)
+ log_debug_errno(errno, "Failure to iterate NSS user database, ignoring: %m");
+
+ iterator->nss_iterating = false;
+ endpwent();
+ }
+
+ r = userdb_process(iterator, ret, NULL, NULL, NULL);
+
+ if (r < 0) {
+ if (iterator->synthesize_root) {
+ iterator->synthesize_root = false;
+ iterator->n_found++;
+ return synthetic_root_user_build(ret);
+ }
+
+ if (iterator->synthesize_nobody) {
+ iterator->synthesize_nobody = false;
+ iterator->n_found++;
+ return synthetic_nobody_user_build(ret);
+ }
+ }
+
+ /* if we found at least one entry, then ignore errors and indicate that we reached the end */
+ if (r < 0 && iterator->n_found > 0)
+ return -ESRCH;
+
+ return r;
+}
+
+static int synthetic_root_group_build(GroupRecord **ret) {
+ return group_record_build(
+ ret,
+ JSON_BUILD_OBJECT(JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING("root")),
+ JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(0)),
+ JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("intrinsic"))));
+}
+
+static int synthetic_nobody_group_build(GroupRecord **ret) {
+ return group_record_build(
+ ret,
+ JSON_BUILD_OBJECT(JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(NOBODY_GROUP_NAME)),
+ JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(GID_NOBODY)),
+ JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("intrinsic"))));
+}
+
+int groupdb_by_name(const char *name, UserDBFlags flags, GroupRecord **ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *query = NULL;
+ int r;
+
+ if (!valid_user_group_name(name))
+ return -EINVAL;
+
+ r = json_build(&query, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(name))));
+ if (r < 0)
+ return r;
+
+ iterator = userdb_iterator_new(LOOKUP_GROUP);
+ if (!iterator)
+ return -ENOMEM;
+
+ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", false, query, flags);
+ if (r >= 0) {
+ r = userdb_process(iterator, NULL, ret, NULL, NULL);
+ if (r >= 0)
+ return r;
+ }
+
+ if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && !(iterator && iterator->nss_covered)) {
+ r = userdb_nss_compat_disable();
+ if (r >= 0 || r == -EBUSY) {
+ iterator->nss_lock = r;
+
+ r = nss_group_record_by_name(name, ret);
+ if (r >= 0)
+ return r;
+ }
+ }
+
+ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) {
+ if (streq(name, "root"))
+ return synthetic_root_group_build(ret);
+
+ if (streq(name, NOBODY_GROUP_NAME) && synthesize_nobody())
+ return synthetic_nobody_group_build(ret);
+ }
+
+ return r;
+}
+
+int groupdb_by_gid(gid_t gid, UserDBFlags flags, GroupRecord **ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *query = NULL;
+ int r;
+
+ if (!gid_is_valid(gid))
+ return -EINVAL;
+
+ r = json_build(&query, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(gid))));
+ if (r < 0)
+ return r;
+
+ iterator = userdb_iterator_new(LOOKUP_GROUP);
+ if (!iterator)
+ return -ENOMEM;
+
+ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", false, query, flags);
+ if (r >= 0) {
+ r = userdb_process(iterator, NULL, ret, NULL, NULL);
+ if (r >= 0)
+ return r;
+ }
+
+ if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && !(iterator && iterator->nss_covered)) {
+ r = userdb_nss_compat_disable();
+ if (r >= 0 || r == -EBUSY) {
+ iterator->nss_lock = r;
+
+ r = nss_group_record_by_gid(gid, ret);
+ if (r >= 0)
+ return r;
+ }
+ }
+
+ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) {
+ if (gid == 0)
+ return synthetic_root_group_build(ret);
+
+ if (gid == GID_NOBODY && synthesize_nobody())
+ return synthetic_nobody_group_build(ret);
+ }
+
+ return r;
+}
+
+int groupdb_all(UserDBFlags flags, UserDBIterator **ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ int r;
+
+ assert(ret);
+
+ iterator = userdb_iterator_new(LOOKUP_GROUP);
+ if (!iterator)
+ return -ENOMEM;
+
+ iterator->synthesize_root = iterator->synthesize_nobody = !FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE);
+
+ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", true, NULL, flags);
+
+ if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && (r < 0 || !iterator->nss_covered)) {
+ iterator->nss_lock = userdb_nss_compat_disable();
+ if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY)
+ return iterator->nss_lock;
+
+ setgrent();
+ iterator->nss_iterating = true;
+ goto finish;
+ }
+
+ if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE))
+ goto finish;
+
+ return r;
+
+finish:
+ *ret = TAKE_PTR(iterator);
+ return 0;
+}
+
+int groupdb_iterator_get(UserDBIterator *iterator, GroupRecord **ret) {
+ int r;
+
+ assert(iterator);
+ assert(iterator->what == LOOKUP_GROUP);
+
+ if (iterator->nss_iterating) {
+ struct group *gr;
+
+ errno = 0;
+ gr = getgrent();
+ if (gr) {
+ _cleanup_free_ char *buffer = NULL;
+ bool incomplete = false;
+ struct sgrp sgrp;
+
+ if (streq_ptr(gr->gr_name, "root"))
+ iterator->synthesize_root = false;
+ if (gr->gr_gid == GID_NOBODY)
+ iterator->synthesize_nobody = false;
+
+ r = nss_sgrp_for_group(gr, &sgrp, &buffer);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to acquire shadow entry for group %s, ignoring: %m", gr->gr_name);
+ incomplete = ERRNO_IS_PRIVILEGE(r);
+ }
+
+ r = nss_group_to_group_record(gr, r >= 0 ? &sgrp : NULL, ret);
+ if (r < 0)
+ return r;
+
+ if (ret)
+ (*ret)->incomplete = incomplete;
+ return r;
+ }
+
+ if (errno != 0)
+ log_debug_errno(errno, "Failure to iterate NSS group database, ignoring: %m");
+
+ iterator->nss_iterating = false;
+ endgrent();
+ }
+
+ r = userdb_process(iterator, NULL, ret, NULL, NULL);
+ if (r < 0) {
+ if (iterator->synthesize_root) {
+ iterator->synthesize_root = false;
+ iterator->n_found++;
+ return synthetic_root_group_build(ret);
+ }
+
+ if (iterator->synthesize_nobody) {
+ iterator->synthesize_nobody = false;
+ iterator->n_found++;
+ return synthetic_nobody_group_build(ret);
+ }
+ }
+
+ /* if we found at least one entry, then ignore errors and indicate that we reached the end */
+ if (r < 0 && iterator->n_found > 0)
+ return -ESRCH;
+
+ return r;
+}
+
+int membershipdb_by_user(const char *name, UserDBFlags flags, UserDBIterator **ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *query = NULL;
+ int r;
+
+ assert(ret);
+
+ if (!valid_user_group_name(name))
+ return -EINVAL;
+
+ r = json_build(&query, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(name))));
+ if (r < 0)
+ return r;
+
+ iterator = userdb_iterator_new(LOOKUP_MEMBERSHIP);
+ if (!iterator)
+ return -ENOMEM;
+
+ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetMemberships", true, query, flags);
+ if ((r >= 0 && iterator->nss_covered) || FLAGS_SET(flags, USERDB_AVOID_NSS))
+ goto finish;
+
+ iterator->nss_lock = userdb_nss_compat_disable();
+ if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY)
+ return iterator->nss_lock;
+
+ iterator->filter_user_name = strdup(name);
+ if (!iterator->filter_user_name)
+ return -ENOMEM;
+
+ setgrent();
+ iterator->nss_iterating = true;
+
+ r = 0;
+
+finish:
+ if (r >= 0)
+ *ret = TAKE_PTR(iterator);
+ return r;
+}
+
+int membershipdb_by_group(const char *name, UserDBFlags flags, UserDBIterator **ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *query = NULL;
+ _cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
+ int r;
+
+ assert(ret);
+
+ if (!valid_user_group_name(name))
+ return -EINVAL;
+
+ r = json_build(&query, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(name))));
+ if (r < 0)
+ return r;
+
+ iterator = userdb_iterator_new(LOOKUP_MEMBERSHIP);
+ if (!iterator)
+ return -ENOMEM;
+
+ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetMemberships", true, query, flags);
+ if ((r >= 0 && iterator->nss_covered) || FLAGS_SET(flags, USERDB_AVOID_NSS))
+ goto finish;
+
+ iterator->nss_lock = userdb_nss_compat_disable();
+ if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY)
+ return iterator->nss_lock;
+
+ /* We ignore all errors here, since the group might be defined by a userdb native service, and we queried them already above. */
+ (void) nss_group_record_by_name(name, &gr);
+ if (gr) {
+ iterator->members_of_group = strv_copy(gr->members);
+ if (!iterator->members_of_group)
+ return -ENOMEM;
+
+ iterator->index_members_of_group = 0;
+
+ iterator->found_group_name = strdup(name);
+ if (!iterator->found_group_name)
+ return -ENOMEM;
+ }
+
+ r = 0;
+
+finish:
+ if (r >= 0)
+ *ret = TAKE_PTR(iterator);
+
+ return r;
+}
+
+int membershipdb_all(UserDBFlags flags, UserDBIterator **ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ int r;
+
+ assert(ret);
+
+ iterator = userdb_iterator_new(LOOKUP_MEMBERSHIP);
+ if (!iterator)
+ return -ENOMEM;
+
+ r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetMemberships", true, NULL, flags);
+ if ((r >= 0 && iterator->nss_covered) || FLAGS_SET(flags, USERDB_AVOID_NSS))
+ goto finish;
+
+ iterator->nss_lock = userdb_nss_compat_disable();
+ if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY)
+ return iterator->nss_lock;
+
+ setgrent();
+ iterator->nss_iterating = true;
+
+ r = 0;
+
+finish:
+ if (r >= 0)
+ *ret = TAKE_PTR(iterator);
+
+ return r;
+}
+
+int membershipdb_iterator_get(
+ UserDBIterator *iterator,
+ char **ret_user,
+ char **ret_group) {
+
+ int r;
+
+ assert(iterator);
+
+ for (;;) {
+ /* If we are iteratring through NSS acquire a new group entry if we haven't acquired one yet. */
+ if (!iterator->members_of_group) {
+ struct group *g;
+
+ if (!iterator->nss_iterating)
+ break;
+
+ assert(!iterator->found_user_name);
+ do {
+ errno = 0;
+ g = getgrent();
+ if (!g) {
+ if (errno != 0)
+ log_debug_errno(errno, "Failure during NSS group iteration, ignoring: %m");
+ break;
+ }
+
+ } while (iterator->filter_user_name ? !strv_contains(g->gr_mem, iterator->filter_user_name) :
+ strv_isempty(g->gr_mem));
+
+ if (g) {
+ r = free_and_strdup(&iterator->found_group_name, g->gr_name);
+ if (r < 0)
+ return r;
+
+ if (iterator->filter_user_name)
+ iterator->members_of_group = strv_new(iterator->filter_user_name);
+ else
+ iterator->members_of_group = strv_copy(g->gr_mem);
+ if (!iterator->members_of_group)
+ return -ENOMEM;
+
+ iterator->index_members_of_group = 0;
+ } else {
+ iterator->nss_iterating = false;
+ endgrent();
+ break;
+ }
+ }
+
+ assert(iterator->found_group_name);
+ assert(iterator->members_of_group);
+ assert(!iterator->found_user_name);
+
+ if (iterator->members_of_group[iterator->index_members_of_group]) {
+ _cleanup_free_ char *cu = NULL, *cg = NULL;
+
+ if (ret_user) {
+ cu = strdup(iterator->members_of_group[iterator->index_members_of_group]);
+ if (!cu)
+ return -ENOMEM;
+ }
+
+ if (ret_group) {
+ cg = strdup(iterator->found_group_name);
+ if (!cg)
+ return -ENOMEM;
+ }
+
+ if (ret_user)
+ *ret_user = TAKE_PTR(cu);
+
+ if (ret_group)
+ *ret_group = TAKE_PTR(cg);
+
+ iterator->index_members_of_group++;
+ return 0;
+ }
+
+ iterator->members_of_group = strv_free(iterator->members_of_group);
+ iterator->found_group_name = mfree(iterator->found_group_name);
+ }
+
+ r = userdb_process(iterator, NULL, NULL, ret_user, ret_group);
+ if (r < 0 && iterator->n_found > 0)
+ return -ESRCH;
+
+ return r;
+}
+
+int membershipdb_by_group_strv(const char *name, UserDBFlags flags, char ***ret) {
+ _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
+ _cleanup_strv_free_ char **members = NULL;
+ int r;
+
+ assert(name);
+ assert(ret);
+
+ r = membershipdb_by_group(name, flags, &iterator);
+ if (r < 0)
+ return r;
+
+ for (;;) {
+ _cleanup_free_ char *user_name = NULL;
+
+ r = membershipdb_iterator_get(iterator, &user_name, NULL);
+ if (r == -ESRCH)
+ break;
+ if (r < 0)
+ return r;
+
+ r = strv_consume(&members, TAKE_PTR(user_name));
+ if (r < 0)
+ return r;
+ }
+
+ strv_sort(members);
+ strv_uniq(members);
+
+ *ret = TAKE_PTR(members);
+ return 0;
+}
+
+static int userdb_thread_sockaddr(struct sockaddr_un *ret_sa, socklen_t *ret_salen) {
+ static const uint8_t
+ k1[16] = { 0x35, 0xc1, 0x1f, 0x41, 0x59, 0xc6, 0xa0, 0xf9, 0x33, 0x4b, 0x17, 0x3d, 0xb9, 0xf6, 0x14, 0xd9 },
+ k2[16] = { 0x6a, 0x11, 0x4c, 0x37, 0xe5, 0xa3, 0x8c, 0xa6, 0x93, 0x55, 0x64, 0x8c, 0x93, 0xee, 0xa1, 0x7b };
+
+ struct siphash sh;
+ uint64_t x, y;
+ pid_t tid;
+ void *p;
+
+ assert(ret_sa);
+ assert(ret_salen);
+
+ /* This calculates an AF_UNIX socket address in the abstract namespace whose existance works as an
+ * indicator whether to emulate NSS records for complex user records that are also available via the
+ * varlink protocol. The name of the socket is picked in a way so that:
+ *
+ * → it is per-thread (by hashing from the TID)
+ *
+ * → is not guessable for foreign processes (by hashing from the — hopefully secret — AT_RANDOM
+ * value every process gets passed from the kernel
+ *
+ * By using a socket the NSS emulation can be nicely turned off for limited amounts of time only,
+ * simply controlled by the lifetime of the fd itself. By using an AF_UNIX socket in the abstract
+ * namespace the lock is automatically cleaned up when the process dies abnormally.
+ *
+ */
+
+ p = ULONG_TO_PTR(getauxval(AT_RANDOM));
+ if (!p)
+ return -EIO;
+
+ tid = gettid();
+
+ siphash24_init(&sh, k1);
+ siphash24_compress(p, 16, &sh);
+ siphash24_compress(&tid, sizeof(tid), &sh);
+ x = siphash24_finalize(&sh);
+
+ siphash24_init(&sh, k2);
+ siphash24_compress(p, 16, &sh);
+ siphash24_compress(&tid, sizeof(tid), &sh);
+ y = siphash24_finalize(&sh);
+
+ *ret_sa = (struct sockaddr_un) {
+ .sun_family = AF_UNIX,
+ };
+
+ sprintf(ret_sa->sun_path + 1, "userdb-%016" PRIx64 "%016" PRIx64, x, y);
+ *ret_salen = offsetof(struct sockaddr_un, sun_path) + 1 + 7 + 32;
+
+ return 0;
+}
+
+int userdb_nss_compat_is_enabled(void) {
+ _cleanup_close_ int fd = -1;
+ union sockaddr_union sa;
+ socklen_t salen;
+ int r;
+
+ /* Tests whether the NSS compatibility logic is currently turned on for the invoking thread. Returns
+ * true if NSS compatibility is turned on, i.e. whether NSS records shall be synthesized from complex
+ * user records. */
+
+ r = userdb_thread_sockaddr(&sa.un, &salen);
+ if (r < 0)
+ return r;
+
+ fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0);
+ if (fd < 0)
+ return -errno;
+
+ /* Try to connect(). This doesn't do anything really, except that it checks whether the socket
+ * address is bound at all. */
+ if (connect(fd, &sa.sa, salen) < 0) {
+ if (errno == ECONNREFUSED) /* the socket is not bound, hence NSS emulation shall be done */
+ return true;
+
+ return -errno;
+ }
+
+ return false;
+}
+
+int userdb_nss_compat_disable(void) {
+ _cleanup_close_ int fd = -1;
+ union sockaddr_union sa;
+ socklen_t salen;
+ int r;
+
+ /* Turn off the NSS compatibility logic for the invoking thread. By default NSS records are
+ * synthesized for all complex user records looked up via NSS. If this call is invoked this is
+ * disabled for the invoking thread, but only for it. A caller that natively supports the varlink
+ * user record protocol may use that to turn off the compatibility for NSS lookups. */
+
+ r = userdb_thread_sockaddr(&sa.un, &salen);
+ if (r < 0)
+ return r;
+
+ fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+ if (fd < 0)
+ return -errno;
+
+ if (bind(fd, &sa.sa, salen) < 0) {
+ if (errno == EADDRINUSE) /* lock already taken, convert this into a recognizable error */
+ return -EBUSY;
+
+ return -errno;
+ }
+
+ return TAKE_FD(fd);
+}
diff --git a/src/shared/userdb.h b/src/shared/userdb.h
new file mode 100644
index 0000000000..4288b0ff95
--- /dev/null
+++ b/src/shared/userdb.h
@@ -0,0 +1,41 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+#include
+
+#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);
diff --git a/src/test/test-user-util.c b/src/test/test-user-util.c
index fde8336bf9..6bfe7dc7e1 100644
--- a/src/test/test-user-util.c
+++ b/src/test/test-user-util.c
@@ -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"
diff --git a/src/userdb/meson.build b/src/userdb/meson.build
new file mode 100644
index 0000000000..2f7e1accbf
--- /dev/null
+++ b/src/userdb/meson.build
@@ -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())
diff --git a/src/userdb/userdbctl.c b/src/userdb/userdbctl.c
new file mode 100644
index 0000000000..9083797856
--- /dev/null
+++ b/src/userdb/userdbctl.c
@@ -0,0 +1,790 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+#include
+
+#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);
diff --git a/src/userdb/userdbd-manager.c b/src/userdb/userdbd-manager.c
new file mode 100644
index 0000000000..2836ca0409
--- /dev/null
+++ b/src/userdb/userdbd-manager.c
@@ -0,0 +1,302 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+
+#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);
+}
diff --git a/src/userdb/userdbd-manager.h b/src/userdb/userdbd-manager.h
new file mode 100644
index 0000000000..0bf67fe55b
--- /dev/null
+++ b/src/userdb/userdbd-manager.h
@@ -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);
diff --git a/src/userdb/userdbd.c b/src/userdb/userdbd.c
new file mode 100644
index 0000000000..978fd1d788
--- /dev/null
+++ b/src/userdb/userdbd.c
@@ -0,0 +1,56 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+#include
+
+#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);
diff --git a/src/userdb/userwork.c b/src/userdb/userwork.c
new file mode 100644
index 0000000000..df11b5b984
--- /dev/null
+++ b/src/userdb/userwork.c
@@ -0,0 +1,778 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+#include
+
+#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);
diff --git a/units/meson.build b/units/meson.build
index 4ad64d12f2..82c399bee8 100644
--- a/units/meson.build
+++ b/units/meson.build
@@ -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/'],
diff --git a/units/systemd-userdbd.service.in b/units/systemd-userdbd.service.in
new file mode 100644
index 0000000000..e30ed2109e
--- /dev/null
+++ b/units/systemd-userdbd.service.in
@@ -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@
diff --git a/units/systemd-userdbd.socket b/units/systemd-userdbd.socket
new file mode 100644
index 0000000000..1c749ea1d2
--- /dev/null
+++ b/units/systemd-userdbd.socket
@@ -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