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-systemd libnss_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: + + + + + user USER… + + List all known users records or show details of one or more specified user + records. Use to tweak output mode. + + + + group GROUP… + + List all known group records or show details of one or more specified group + records. Use to tweak output mode. + + + + users-in-group GROUP… + + 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-user USER… + + 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