diff --git a/.mkosi/mkosi.fedora b/.mkosi/mkosi.fedora index b0673f707a399..2de0976bc38de 100644 --- a/.mkosi/mkosi.fedora +++ b/.mkosi/mkosi.fedora @@ -38,10 +38,12 @@ BuildPackages= libblkid-devel libcap-devel libcurl-devel + libfdisk-devel libgcrypt-devel libidn2-devel libmicrohttpd-devel libmount-devel + libpwquality-devel libseccomp-devel libselinux-devel libtool @@ -51,6 +53,7 @@ BuildPackages= lz4-devel m4 meson + openssl-devel pam-devel pcre2-devel pkgconfig @@ -61,6 +64,7 @@ BuildPackages= xz-devel Packages= + e2fsprogs libidn2 BuildDirectory=mkosi.builddir diff --git a/CRYPTSETUP b/CRYPTSETUP new file mode 100644 index 0000000000000..bca0e07b80615 --- /dev/null +++ b/CRYPTSETUP @@ -0,0 +1,63 @@ +# Using a Yubikey for unlocking arbitrary LUKS devices. +# +# A few notes on the used parameters: +# +# → We use RSA (and not ECC), since Yubikeys support PKCS#11 Decrypt() only for +# RSA keys (ECC keys can only be used for signing?) +# → We use RSA2048, which is the longest key size current Yubikeys support for +# RSA +# → LUKS key size must be << 2048bit due to RSA padding, hence we use 128 bytes +# → We use Yubikey key slot 9d, since that's apparently the keyslot to use for +# decryption purposes, see: +# https://developers.yubico.com/PIV/Introduction/Certificate_slots.html + +# Make sure noone can read the files we generate, but us +umask 077 + +# Clear the Yubikey from any old keys +ykman piv reset + +# Generate a new private/public key pair on th device, store the public key in +# 'pubkey.pem'. +ykman piv generate-key -a RSA2048 9d pubkey.pem + +# Create a self-signed certificate from this public key, and store it on the +# device. +ykman piv generate-certificate --subject "Knobelei" 9d pubkey.pem + +# Check if the newly create key on the Yubikey shows up as token in +# PKCS#11. Have a look at the output, and copy the resulting token URI to the +# clipboard. +p11tool --list-tokens + +# Generate a (secret) random key to use as LUKS decryption key. +dd if=/dev/urandom of=plaintext.bin bs=128 count=1 + +# Encrypt this newly generated LUKS decryption key using the public key whose +# private key is on the Yubikey, store the result in +# /etc/encrypted-luks-key.bin, where we'll look for it during boot. +openssl rsautl -encrypt -pubin -inkey pubkey.pem -in plaintext.bin -out /etc/encrypted-luks-key.bin + +# Configure the LUKS decryption key on the LUKS device. We use very low pbkdf +# settings since the key already has quite a high quality (it comes directly +# from /dev/urandom after all), and thus we don't need to do much key +# derivation. +cryptsetup luksAddKey /dev/sda1 plaintext.bin --pbkdf-memory=32 --pbkdf-parallel=1 --pbkdf-force-iterations=4 + +# Now securely delete the plain text LUKS key, we don't need it anymore, and +# since it contains secret key material it should be removed from disk +# thoroughly. +shred -u plaintext.bin + +# We don't need the public key anymore either, let's remove it too. Since this +# one is not security sensitive we just do a regular "rm" here. +rm pubkey.pem + +# Test: Let's run systemd-cryptsetup to test if this all worked. The option +# string should contain the full PKCS#11 URI we have in the clipboard, it tells +# the tool how to decypher the encrypted LUKS key. +systemd-cryptsetup attach mytest /dev/sda1 /etc/encrypted-luks-key.bin 'pkcs11-uri=pkcs11:…' + +# If that worked, let's now add the same line persistently to /etc/crypttab, +# for the future. +echo "mytest /dev/sda1 /etc/encrypted-luks-key 'pkcs11-uri=pkcs11:…' >> /etc/crypttab diff --git a/TODO b/TODO index c5b5b86057ba3..1538e15d10d49 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,14 @@ Janitorial Clean-ups: Features: +* socket units: allow creating a udev monitor socket with ListenDevices= or so, + with matches, then actviate app thorugh that passing socket oveer + +* cryptsetup: allow encoding key directly in /etc/crypttab, maybe with a + "base64:" prefix. Useful in particular for pkcs11 mode. + +* cryptsetup: add logic to wait for pkcs11 token + * coredump: maybe when coredumping read a new xattr from /proc/$PID/exe that may be used to mark a whole binary as non-coredumpable. Would fix: https://bugs.freedesktop.org/show_bug.cgi?id=69447 @@ -395,15 +403,6 @@ Features: * maybe introduce gpt auto discovery for /var/tmp? -* maybe add gpt-partition-based user management: each user gets his own - LUKS-encrypted GPT partition with a new GPT type. A small nss module - enumerates users via udev partition enumeration. UIDs are assigned in a fixed - way: the partition index is added as offset to some fixed base uid. User name - is stored in GPT partition name. A PAM module authenticates the user via the - LUKS partition password. Benefits: strong per-user security, compatibility - with stateless/read-only/verity-enabled root. (other idea: do this based on - loopback files in /home, without GPT involvement) - * gpt-auto logic: introduce support for discovering /var matching an image. For that, use a partition type UUID that is hashed from the OS name (as encoded in /etc/os-release), the architecture, and 4 new bits from the gpt flags diff --git a/TODO-HOME b/TODO-HOME new file mode 100644 index 0000000000000..4263f637e0826 --- /dev/null +++ b/TODO-HOME @@ -0,0 +1,51 @@ +PRIMARILY: + +- pkcs11 smart card support: pin certificate in record, then for auth, generate random data, ask smartcard to sign it, then verify signature. For LUKS key store encrypted key in record, decrypt it with smartcard. +- write fscrypt key into client's keyring +- don't use argon2 with pkcs#11 cryptsetup mode +- move the ssh authorized keys stuff from homectl to userdbctl +- honour passwordChangeNow, and unset it in homectl passwd +- fix borked yubikey cryptsetup stuff? + +Before first release: +- unit file lockdown (caps, …) +- extend test case to cover more +- man pages: + cryptsetup pkcs11 + pam_systemd update + nss-systemd update +- blog story +- fix userwork and homework paths +- drop LOG_DEBUG being forced in userdbd and homed +- drop debug=true being forced in pam_systemd_home +- performance data + +Later: +- when user tries to log into record signed by unrecognized key, automatically add key to our chain after polkit auth +- hook up machined/nspawn users with a varlink user query interface +- rollback when resize fails mid-operation +- forget key on suspend (requires gnome rework so that lock screen runs outside of uid) +- resize on login? +- update LUKS password on login if we find there's a password that unlocks the JSON record but not the LUKS device. +- always fstrim on logout? +- compare with accounts daemon +- create on activate? +- properties: icon url?, preferred session type?, administrator bool (which translates to 'wheel' membership)?, address?, telephone?, vcard?, samba stuff?, parental controls? +- communicate clearly when usb stick is safe to remove. probably involves + beefing up logind to make pam session close hook synchronous and wait until + systemd --user is shut down. +- logind: maybe keep a "busy fd" as long as there's a non-released session around or the user@.service +- fscrypt key mgmt (maybe in xattr?) +- shrink fs on logout? +- maybe make automatic, read-only, time-based reflink-copies of LUKS disk images (think: time machine) +- distuingish destroy / remove (i.e. currently we can unregister a user, unregister+remove their home directory, but not just remove their home directory) +- in systemd's PAMName= logic: query passwords with ssh-askpassword, so that we can make "loginctl set-linger" mode work +- fingerprint authentication, pattern authentication, … +- make sure "classic" user records can also be managed by homed +- description field for groups +- make size of $XDG_RUNTIME_DIR configurable in user record +- reuse pwquality magic in firstboot +- query password from kernel keyring first +- update in absence +- add a "access mode" + "fstype" field to the "status" section of json identity records reflecting the actually used access mode and fstype, even on non-luks backends +- move acct mgmt stuff from pam_systemd_home to pam_systemd? diff --git a/docs/GROUP_RECORD.md b/docs/GROUP_RECORD.md new file mode 100644 index 0000000000000..55cb2f064974a --- /dev/null +++ b/docs/GROUP_RECORD.md @@ -0,0 +1,156 @@ +--- +title: JSON Group Records +--- + +# 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 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/HOME_DIRECTORY.md b/docs/HOME_DIRECTORY.md new file mode 100644 index 0000000000000..b07e0c4886f64 --- /dev/null +++ b/docs/HOME_DIRECTORY.md @@ -0,0 +1,163 @@ +--- +title: Home Directories +--- + +# Home Directories + +[`systemd-homed.service(8)`](https://www.freedesktop.org/software/systemd/man/systemd-homed.service.html) +manages home directories of regular ("human") users. Each directory it manages +encapsulates both the data store and the user record of the user so that it +comprehensively describes the user account, and is thus naturally portable +between systems without any further, external metadata. This document describes +the format used by these home directories, in context of the storage mechanism +used. + +## General Structure + +Inside of the home directory a file `~/.identity` contains the JSON formatted +user record of the user. It follows the format defined in [`JSON User +Records`](https://systemd.io/USER_RECORDS). It is recommended to bring the +record into 'normalized' form (i.e. all objects should contain their fields +sorted alphabetically by their key) before storing it there, though this is not +required nor enforced. Since the user record is cryptographically signed the +user cannot make modifications to the file on their own (at least not without +corrupting it, or knowing the private key used for signing the record). Note +that user records are stored here without their `binding`, `status` and +`secret` sections, i.e. only with the sections included in the signature plus +the signature section itself. + +## Storage Mechanism: Plain Directory/`btrfs` Subvolume + +If the plain directory or `btrfs` subvolume storage mechanism of +`systemd-homed` is used (i.e. `--storage=directory` or `--storage=subvolume` on +the +[`homectl(1)`](https://www.freedesktop.org/software/systemd/man/homectl.html) +command line) the home directory requires no special set-up besides including +the user record in the `~/.identity` file. + +It is recommended to name home directories managed this way by +`systemd-homed.service` by the user name, suffixed with `.homedir` (example: +`lennart.homedir` for a user `lennart`) but this is not enforced. When the user +is logged in the directory is generally mounted to `/home/$USER` (in our +example: `/home/lennart`), thus dropping the suffix while the home directory is +active. `systemd-homed` will automatically discover home directories named this +way in `/home/*.homedir` and synthesize NSS user records for them as they show +up. + +## Storage Mechanism: `fscrypt` Directories + +This storage mechanism is mostly identical to the plain directory storage +mechanism, except that the home directory is encrypted using `fscrypt`. (Use +`--storage=fscrypt` on the `homectl` command line.) There is currently no key +management in place for this, the `fscrypt` encryption key is directly derived +from the supplied user password using PBKDF2. Due to this password changes are +currently not supported if this storage mechanism is used. The salt value for +the key derivation function is stored in the `trusted.fscrypt_salt` extended +attribute, encoded in base64. + +## Storage Mechanism: `cifs` Home Directories + +In this storage mechanism the home directory is mounted from a CIFS server and +service at login, configured inside the user record. (Use `--storage=cifs` on +the `homectl` command line.) The local password of the user is used to log into +the CIFS service. The directory share needs to contain the user record in +`~/.identity` as well. Note that this means that the user record needs to be +registered locally before it can be mounted for the first time, since CIFS +domain and server information needs to be known *before* the mount. Note that +for all other storage mechanisms it is entirely sufficient if the directories +or storage artifacts are placed at the right locations — all information to +activate them can be derived automatically from their mere availability. + +## Storage Mechanism: `luks` Home Directories + +This is the most advanced and most secure storage mechanism and consists of a +Linux file system inside a LUKS2 volume inside a loopback file (or on removable +media). (Use `--storage=luks` on the `homectl` command line.) Specifically: + +* The image contains a GPT partition table. For now it should only contain a + single partition, and that partition must have the type UUID + `773f91ef-66d4-49b5-bd83-d683bf40ad16`. It's partition label must be the + user name. + +* This partition must contain a LUKS2 volume, whose label must be the user + name. The LUKS2 volume must contain a LUKS2 token field of type + `systemd-homed`. The JSON data of this token must have a `record` field, + containing a string with base64-encoded data. This data is the JSON user + record, in the same serialization as in `~/.identity`, though encrypted. The + JSON data of this token must also have an `iv` field, which contains a + base64-encoded binary initialization vector for the encryption. The + encryption used is the same as the LUKS2 volume itself uses, unlocked by the + same volume key, but based on its own IV. + +* Inside of this LUKS2 volume must be a Linux file system, one of `ext4`, + `btrfs` and `xfs`. The file system label must be the user name. + +* This file system should contain a single directory named after the user. This + directory will become the home directory of the user when activated. It + contains a second copy of the user record in the `~/.identity` file, like in + the other storage mechanisms. + +The image file should either reside in a directory `/home/` on the system, +named after the user, suffixed with `.home`. When activated the container home +directory is mounted to the same path, though with the `.home` suffix +dropped. (e.g.: the loopback file `/home/waldo.home` is mounted to +`/home/waldo` while activated.) When the image is stored on removable media +(such as a USB stick) the image file can be directly `dd`'ed onto it, the +format is unchanged. The GPT envelope should ensure the image is properly +recognizable as a home directory both when used in a loopback file and on a +removable USB stick. + +Rationale for the GPT partition table envelope: this way the image is nicely +discoverable and recognizable already by partition managers as a home +directory. Moreover, when copied onto a USB stick the GPT envelope makes sure +the stick is properly recognizable as a portable home directory +medium. (Moreover it allows to embed additional partitions later on, for +example for allowing a multi-purpose USB stick that contains both a home +directory and a generic storage volume.) + +Rationale for including the encrypted user record in the the LUKS2 header: +Linux kernel file system implementations are generally not robust towards +maliciously formatted file systems; there's a good chance that file system +images can be used as attack vectors, exploiting the kernel. Thus it is +necessary to validate the home directory image *before* mounting it and +establishing a minimal level of trust. Since the user record data is +cryptographically signed and user records not signed with a recognized private +key are not accepted a minimal level of trust between the system and the home +directory image is established. + +Rationale for storing the home directory one level below to root directory of +the contained file system: this way special directories such as `lost+found/` +do not show up in the user's home directory. + +## Algorithm + +Regardless of the storage mechanism used, an activated home directory +necessarily involves a mount point to be established. In case of the +directory-based storage mechanisms (`directory`, `subvolume` and `fscrypt`) +this is a bind mount, in case of `cifs` this is a CIFS network mount, and in +case of the LUKS2 backend a regular block device mount of the file system +contained in the LUKS2 image. By requiring a mount for all cases (even for +those that already are a directory) a clear logic is defined to distuingish +active and inactive home directories, so that the directories become +inaccessible under their regular path the instant they are +deactivated. Moreover, the `nosuid`, `nodev` and `noexec` flags configured in +the user record are applied when the bind mount is established. + +During activation, the user records retained on the host, the user record +stored in the LUKS2 header (in case of the LUKS2 storage mechanism) and the +user record stored inside the home directory in `~/.identity` are +compared. Activation is only permitted if they match the same user and are +signed by a recognized key. When the three instances differ in `lastChangeUSec` +field, the newest record wins, and is propagated to the other two locations. + +During activation the file system checker (`fsck`) appropriate for the +selected file system is automatically invoked, ensuring the file system is in a +healthy state before it is mounted. + +If the UID assigned to a user does not match the owner of the home directory in +the file system, the home directory is automatically and recursively `chown()`ed +to the correct UID. + +Depending on the `discard` setting of the user record either the backing +loopback file is `fallocate()`ed during activation, or the mounted file system +is `FITRIM`ed after mounting, to ensure the setting is correctly enforced. diff --git a/docs/UIDS-GIDS.md b/docs/UIDS-GIDS.md index 480ee231e7431..6cb0e144fbebc 100644 --- a/docs/UIDS-GIDS.md +++ b/docs/UIDS-GIDS.md @@ -94,7 +94,15 @@ but downstreams are strongly advised against doing that.) `systemd` defines a number of special UID ranges: -1. 61184…65519 → UIDs for dynamic users are allocated from this range (see the +1. 60001…60513 → UIDs for home directories managed by + [`systemd-homed.service(8)`](https://www.freedesktop.org/software/systemd/man/systemd-homed.service.html). UIDs + from this range are automatically assigned to any home directory discovered, + and persisted locally on first login. On different systems the same user + might get different UIDs assigned in case of conflict, though it is + attempted to make UID assignments stable, by hashing them from the user + name. + +2. 61184…65519 → UIDs for dynamic users are allocated from this range (see the `DynamicUser=` documentation in [`systemd.exec(5)`](https://www.freedesktop.org/software/systemd/man/systemd.exec.html)). This range has been chosen so that it is below the 16bit boundary (i.e. below @@ -109,7 +117,7 @@ but downstreams are strongly advised against doing that.) user record resolving works correctly without those users being in `/etc/passwd`. -2. 524288…1879048191 → UID range for `systemd-nspawn`'s automatic allocation of +3. 524288…1879048191 → UID range for `systemd-nspawn`'s automatic allocation of per-container UID ranges. When the `--private-users=pick` switch is used (or `-U`) then it will automatically find a so far unused 16bit subrange of this range and assign it to the container. The range is picked so that the upper @@ -230,7 +238,8 @@ the artifacts the container manager persistently leaves in the system. | 5 | `tty` group | `systemd` | `/etc/passwd` | | 6…999 | System users | Distributions | `/etc/passwd` | | 1000…60000 | Regular users | Distributions | `/etc/passwd` + LDAP/NIS/… | -| 60001…61183 | Unused | | | +| 60001…60513 | Human Users (homed) | `systemd` | `nss-systemd` +| 60514…61183 | Unused | | | | 61184…65519 | Dynamic service users | `systemd` | `nss-systemd` | | 65520…65533 | Unused | | | | 65534 | `nobody` user | Linux | `/etc/passwd` + `nss-systemd` | diff --git a/docs/USER_GROUP_API.md b/docs/USER_GROUP_API.md new file mode 100644 index 0000000000000..9d56085a8f9a2 --- /dev/null +++ b/docs/USER_GROUP_API.md @@ -0,0 +1,235 @@ +--- +title: Varlink User/Group Lookup API +--- + +# Varlink User/Group Lookup API + +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 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 a 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. + +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. + +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. + +## 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 this Varlink API, either way +all records are accessible via both APIs, due to the two-way compatibility. 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 on 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 on 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 the existence or non-existence of a record can be returned nor any user +record at all. + +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. 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 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 services +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 parallel 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 be 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 0000000000000..b7338e96f6c34 --- /dev/null +++ b/docs/USER_RECORD.md @@ -0,0 +1,902 @@ +--- +title: JSON User Records +--- + +# 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 advanced JSON-based format. 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 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 thinkable: + +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 +[Varlink User/Group Lookup API](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 is 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 insert 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 | Persisted | 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 | + +## 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 the umask is noted in octal, but JSON's integers are +generally written in decimal, hence that's what this is too. The specified +value should be in the valid range for umasks, i.e. 0000…0777 (in octal), or +0…511 (in decimal). 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 initialized the `$TZ` environment variable from this +string. The string hence should be in a format compatible with this environment +variable, 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 initialized 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 +this, 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 with 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` use 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 to 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, which is +taken relative to the available disk space on the selected storage medium. 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. + +`fscryptSalt` → A string containing base64 encoded data. Selects the the salt +bytes to use for the PBKDF2 key derivation function when using `fscrypt` as +storage mechanism for the home directory. + +`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. + +`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. + +## 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`, `fscryptSalt`, `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`. + +## 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 use 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`, `fscryptSalt`, `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 indicates +that indicates 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. + +## 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. + +## 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 + } + } +} +``` diff --git a/factory/etc/nsswitch.conf b/factory/etc/nsswitch.conf index 5470993e34b2a..e7365cd142650 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/factory/etc/pam.d/system-auth b/factory/etc/pam.d/system-auth index 747efc1fbdb69..02544808e6246 100644 --- a/factory/etc/pam.d/system-auth +++ b/factory/etc/pam.d/system-auth @@ -4,16 +4,19 @@ # unmodified you are not building systems safely and securely. auth sufficient pam_unix.so nullok try_first_pass +auth sufficient pam_systemd_home.so auth required pam_deny.so account required pam_nologin.so account sufficient pam_unix.so -account required pam_permit.so +account sufficient pam_systemd_home.so password sufficient pam_unix.so nullok sha512 shadow try_first_pass try_authtok +password sufficient pam_systemd_home.so password required pam_deny.so -session optional pam_keyinit.so revoke -session optional pam_loginuid.so +-session optional pam_systemd_home.so -session optional pam_systemd.so -session sufficient pam_unix.so +session required pam_unix.so diff --git a/man/homectl.xml b/man/homectl.xml new file mode 100644 index 0000000000000..79a8e3fdd68d8 --- /dev/null +++ b/man/homectl.xml @@ -0,0 +1,742 @@ + + + + + + + + homectl + systemd + + + + homectl + 1 + + + + homectl + Create, remove, change or inspect home directories + + + + + homectl + OPTIONS + COMMAND + NAME + + + + + Description + + homectl may be used to create, remove, change or inspect a user's home + directory. It's primarily a command interfacing with + systemd-homed.service8 + which manages home directories of users. + + Home directories managed by systemd-homed.service are self-contained, and thus + include the user's full metadata record in the home's data storage itself, making them easily migratable + between machines. In particular a home directory in itself describes a matching user record, and every + user record managed by systemd-homed.service also implies existance and + encapsulation of a home directory. The user account and home directory hence become the same concept. The + following backing storage mechanisms are supported: + + + Individual LUKS2 encrypted loopback files for each user, located in + /home/*.home. At login the file systems contained in these files are mounted, + after the LUKS2 encrypted volume is attached. The user's password is identical to the encryption + passphrase of the LUKS2 volume. Access to data without preceeding user authentication is thus not + possible, not even for the systems administrator. This storage mechanism provides the strongest data + security and is thus recommended. + + Similar, but the LUKS2 encrypted file system is located on regular block device, such + as an USB storage stick. In this mode home directories and all data they include are nicely migratable + between machines, simply by plugging the USB stick into different systems at different + times. + + An encrypted directory using fscrypt on file systems that support it + (at the moment this is primarily ext4), located in + /home/*.homedir. This mechanism also provides encryption, but substantially + weaker than the two mechanisms described above, as most file system metadata is unprotected. Moreover + it currently does not support changing user passwords once the home directory has been + created. + + A btrfs subvolume for each user, also located in + /home/*.homedir. This provides no encryption, but good quota + support. + + A regular directory for each user, also located in + /home/*.homedir. This provides no encryption, but is a suitable fallback + available on all machines, even where LUKS2, fscrypt or btrfs + support is not available. + + An individual Windows file share (CIFS) for each user. + + + Note that systemd-homed.service and homectl will not manage + "classic" UNIX user accounts as created with useradd8 or + similar tools. In particular, this functionality is not suitable for managing system users (i.e. users + with a UID below 1000) but is exclusive to regular ("human") users. + + Note that users/home directories managed via systemd-homed.service do not show + up in /etc/passwd and similar files, they are synthesized via glibc NSS during + runtime. They are thus resolvable and may be enumerated via the getent1 + tool. + + This tool interfaces directly with systemd-homed.service, and may execute + specific commands on the home directories it manages. Since every home directory managed that way also + defines a JSON user and group record these home directories may also be inspected and enumerated via + userdbctl1. + + + + Options + + The following general options are understood (further options that control the various properties + of user records managed by systemd-homed.service are documented further + down): + + + + + FILE + + Read the user's JSON record from the specified file. If passed as + - reads the user record from standard input. The supplied JSON object must follow + the structure documented on JSON User + Records. This option may be used in conjunction with the create and + update commands (see below), where it allows configuring the user record in JSON + as-is, instead of setting the individual user record properties (see below). + + + + FORMAT + + + Controls whether to output the user record in JSON format, if the + inspect command (see below) is used. Takes one of pretty, + short or off. If pretty human-friendly + whitespace and newlines are inserted in the output to make the JSON data more readable. If + short all superfluous whitespace is suppressed. If off (the + default) the user information is not shown in JSON format but in a friendly human readable formatting + instead. The option picks pretty when run interactively and + short otherwise. + + + + FORMAT + + + + When used with the inspect verb in JSON mode (see above) may be + used to suppress certain aspects of the JSON user record on output. Specifically, if + stripped format is used the binding and runtime fields of the record are + removed. If minimal format is used the cryptographic signature is removed too. If + full format is used the full JSON record is shown (this is the default). This + option is useful for copying an existing user record to a different system in order to create a + similar user there with the same settings. Specifically: homectl inspect -EE | ssh + root@othersystem homectl create -i- may be used as simple command line for replicating a + user on another host. is equivalent to , + to . Note that when replicating user + accounts user records acquired in stripped mode will retain the original + cryptographic signatures and thus may only modified when the private key to update them is available + on the destination machine. When replicating users in minimal mode the signature + is remove during the replication and thus it is implicitly signed with the key of the destination + machine and thus may be updated there without any private key replication. + + + + + + + + + + + + + + + User Record Properties + + The following options control various properties of the user records/home directories that + systemd-homed.service manages. These switches may be used in conjunction with the + create and update commands for configuring various aspects of the + home directory and the user account: + + + + + NAME + NAME + + The real name for the user. This corresponds with the GECOS field on classic UNIX NSS + records. + + + + REALM + + The realm for the user. The realm associates a user with a specific organization or + installation, and allows distuingishing users of the same name defined in different contexts. The realm + can be any string that also qualifies as valid DNS domain name, and it is recommended to use the + organization's or installation's domain name for this purpose, but this is not enforced nor + required. On each system only a single user of the same name may exist, and if a user with the same + name and realm is seen it is assumed to refer to the same user while a user with the same name but + different realm is considered a different user. Assigning a realm to a user is + optional. + + + + EMAIL + + Takes an electronic mail address to associate with the user. On log-in the + $EMAIL environment variable is initialized from this value. + + + + TEXT + + Takes location specification for this user. This is free-form text, which might or + might not be usable by geo-location applications. Example: or + + + + TEXT + + Takes a password hint to store alongside the user record. This string is stored + accessible only to privileged users and the user itself and may not be queried by other users. + Example: + + + + ICON + + Takes an icon name to associate with the user, following the scheme defined by the Icon Naming + Specification. + + + + PATH + PATH + + Takes a path to use as home directory for the user. Note that this is the directory + the user's home directory is mounted to while the user is logged in. This is not where the user's + data is actually stored, see for that. If not specified defaults to + /home/$USER. + + + + UID + + Takes a preferred numeric UNIX UID to use for this user. Note that + systemd-homed may assign a user a different UID if needed if the UID is already in + use. The specified UID must be outside of the system user range. It is recommended to use the + 60001…60513 UID range for this purpose. If not specified the UID is automatically picked. When + logging in and the home directory is found to be owned by a UID not matching the user's assigned one + the home directory and all files and directories inside it will have their ownership + changed automatically before login completes. + + Note that users managed by systemd-homed always have a matching group + associated with the same name as well as a GID matching the UID of the user. Thus, configuring the + GID separately is not permitted. + + + + GROUP + GROUP + + Takes a comma-separated list of auxiliary UNIX groups this user shall belong + to. Example: to provide the user with administrator + privileges. Note that systemd-homed does not manage any groups besides a group + matching the user in name and numeric UID/GID. Thus any groups listed here must be registered + independently, for example with + groupadd8. If + non-existant groups are listed they are ignored. This option may be used more than once in case all + specified group lists are combined. + + + + PATH + + Takes a file system path to a directory. Specifies the skeleton directory to + initialize the home directory with. All files and directories in the specified are copied into any + newly create home directory. If not specified defaults to + /etc/skel/. + + + + SHELL + + Takes a file system path. Specifies the shell binary to execute on terminal + logins. If not specified defaults to /bin/bash. + + + + VARIABLE=VALUE + + Takes an environment variable assignment to set for all user processes. Note that a + number of other settings also result in environment variables to be set for the user, including + , and . May be used + multiple times to set multiple environment variables. + + + + TIMEZONE + + Takes a timezone specification as string that sets the timezone for the specified + user. When the user logs in the $TZ environment variable is initialized from this + setting. Example: . + + + + LANG + + Takes a specifier indicating the preferred language of the user. The + $LANG environment variable is initialized from this value on login, and thus a + value suitable for this environment variable is accepted here, for example + + + + + KEYS + Either takes a SSH authorized key line to associate with the user record or a + @ character followed by a path to a file to read one or more such lines from. SSH + keys configured this way are made available to SSH to permit access to this home directory and user + record. This option may be used more than once to configure multiple SSH keys. + + + + BOOLEAN + + Takes a boolean argument. Specifies whether this user account shall be locked. If + true logins into this account are prohibited, if false (the default) they are permitted (of course, + only if authorization otherwise succeeds). + + + + TIMESTAMP + TIMESTAMP + + These options take a timestamp string, in the format documented in + systemd.time7 and + configures points in time before and after logins into this account are not + permitted. + + + + SECS + NUMBER + + Configures a rate limit on authentication attempts for this user. If the user + attempts to authenticate more often than the specified number, on a specific system, within the + specified time interval authentication is refused until the time interval passes. Defaults to 10 + times per 1min. + + + + BOOL + + + Takes a boolean argument. Configures whether to enforce the system's password policy + for this user, regarding quality and strength of selected passwords. Defaults to + on. is short for + . + + + + BYTES + Either takes a size in bytes as argument (possibly using the usual K, M, G, … + suffixes for 1024 base values), or a percentage value and configures the disk space to assign to the + user. If a percentage value is specified (i.e. the argument suffixed with %) it is + taken relative to the available disk space of the backing file system. If the LUKS2 backend is used + this configures the size of the loopback file and file system contained therein. For the other + storage backends configures disk quota using the filesystem's native quota logic, if available. If + not specified, defaults to 85% of the available disk space for the LUKS2 backend and to no quota for + the others. + + + + MODE + + Takes a UNIX file access mode written in octal. Configures the access mode of the + home directory itself. Note that this is only used when the directory is first created, and the user + may change this any time afterwards. Example: + + + + + MASK + + Takes the access mode mask (in octal syntax) to apply to newly created files and + directories of the user ("umask"). If set this controls the initial umask set for all login sessions of + the user, possibly overriding the system's defaults. + + + + NICE + + Takes the numeric scheduling priority ("nice level") to apply to the processes of the user at login + time. Takes a numeric value in the range -20 (highest priority) to 19 (lowest priority). + + + + LIMIT=VALUE:VALUE + + Allows configuration of resource limits for processes of this user, see getrlimit2 + for details. Takes a resource limit name (e.g. LIMIT_NOFILE) followed by an equal + sign, followed by a numeric limit. Optionally, separated by colon a second numeric limit may be + specified. If two are specified this refers to the soft and hard limits, respectively. If only one + limit is specified the setting sets both limits in one. + + + + TASKS + + Takes a non-zero unsigned integer as argument. Configures the maximum numer of tasks + (i.e. processes and threads) the user may have at any given time. This limit applies to all tasks + forked off the user's sessions, even if they change user identity via su1 or a + similar tool. Use to place a limit on the tasks actually + running under the UID of the user, thus excluding any child processes that might have changed user + identity. This controls the TasksMax= settting of the per-user systemd slice unit + user-$UID.slice. See + systemd.resource-control5 + for further details. + + + + BYTES + BYTES + + Set a limit on the memory a user may take up on a system at any given time in bytes + (the usual K, M, G, … suffixes are supported, to the base of 1024). This includes all memory used by + the user itself and all processes they forked off that changed user credentials. This controls the + MemoryHigh= and MemoryMax= settings of the per-user systemd + slice unit user-$UID.slice. See + systemd.resource-control5 + for further details. + + + + WEIGHT + WEIGHT + + Set a CPU and IO scheduling weights of the processes of the user, including those of + processes forked off by the user that changed user credentials. Takes a numeric value in the range + 1…10000. This controls the CPUWeight= and IOWeight= settings of + the per-user systemd slice unit user-$UID.slice. See + systemd.resource-control5 + for further details. + + + + STORAGE + + Selects the storage mechanism to use for this home directory. Takes one of + luks, fscrypt, directory, + subvolume, cifs. For details about these mechanisms, see + above. If a new home directory is created and the storage type is not specifically specified defaults + to luks if supported, subvolume as first fallback if supported, + and directory if not. + + + + PATH + + Takes a file system path. Configures where to place the user's home directory. When + LUKS2 storage is used refers to the path to the loopback file, otherwise to the path to the home + directory. When unspecified defaults to /home/$USER.home when LUKS storage is + used and /home/$USER.homedir for the other storage mechanisms. Not defined for + the cifs storage mechanism. To use LUKS2 storage on a regular block device (for + example a USB stick) pass the path to the block device here. + + + + TYPE + + When LUKS2 storage is used configures the file system type to use inside the home + directory LUKS2 container. One of ext4, xfs, + btrfs. If not specified defaults to ext4. Note that + xfs is not recommended as its support for file system resizing is too + limited. + + + + BOOL + + When LUKS2 storage is used configures whether to enable the + discard feature of the file system. If enabled the file system on top of the LUKS2 + volume will report empty block information to LUKS2 and the loopback file below, ensuring that empty + space in the home directory is returned to the backing file system below the LUKS2 volume, resulting + in a "sparse" loopback file. This option mostly defaults to off, since this permits over-committing + home directories which results in I/O errors if the underlying file system runs full while the upper + file system wants to allocate a block. Such I/O errors are generally not handled well by file systems + nor applications. When LUKS2 storage is used on top of regular block devices (instead of on top a + loopback file) the discard logic defaults to on. + + + + CIPHER + MODE + BITS + TYPE + ALGORITHM + SECONDS + BYTES + THREADS + + Configures various cryptographic parameters for the LUKS2 storage mechanism. See + cryptsetup8 + for details on the specific attributes. + + + + BOOL + BOOL + BOOL + + Configures the nosuid, nodev and + noexec mount options for the home directories. By default nodev + and nosuid are on, while noexec is off. For details about these + mount options see mount8. + + + + DOMAIN + USER + SERVICE + + Configures the Windows File Sharing (CIFS) domain and user to associate with the home + directory/user account, as well as the file share ("service") to mount as directory. The latter is used when + cifs storage is selected. + + + + SECS + + Configures the time the per-user service manager shall continue to run after the all + sessions of the user ended. The default is configured in + logind.conf5 (for + home directories of LUKS2 storage located on removable media this defaults to 0 though). A longer + time makes sure quick, repetitive logins are more efficient as the user's service manager doesn't + have to be started every time. + + + + BOOL + + Configures whether to kill all processes of the user on logout. The default is + configured in + logind.conf5. + + + + BOOL + + Takes a boolean argument. Configures whether the graphical UI of the system should + automatically log this user in if possible. Defaults to off. If less or more than one user is marked + this way automatic login is disabled. + + + + + + Commands + + The following commands are understood: + + + + + list + + List all home directories (along with brief details) currently managed by + systemd-homed.service. This command is also executed if none is specified on the + command line. + + + + activate USER [USER…] + + Activate one or more home directories. The home directories of each listed user will + be activated and made available under their mount points (typically in + /home/$USER). Note that any home activated this way stays active indefinitely, + until it is explicitly deactivated again (with deactivate, see below), or the user + logs in and out again and it thus is deactivated due to the automatic deactivation-on-logout + logic. + + Activation of a home directory involves various operations that depend on the selected storage + mechanism. If the LUKS2 mechanism is used, this generally involves: setting up a loopback device, + validating and activating the LUKS2 volume, checking the file system, mounting the file system, and + potentiatlly changing the ownership of all included files to the correct UID/GID. + + + + deactivate USER [USER…] + + Deactivate one or more home directories. This undoes the effect of + activate. + + + + inspect USER [USER…] + + Show various details about the specified home directories. This shows various + information about the home directory and its user account, including runtime data such as current + state, disk use and similar. Combine with to show the detailed JSON user + record instead, possibly combined with to suppress certain aspects + of the output. + + + + authenticate USER [USER…] + + Validate authentication credentials of a home directory. This queries the caller for + a password (or similar) and checks that it correctly unlocks the home directory. + + + + create USER + create PATH USER + + Create a new home directory/user account of the specified name. Use the various + user record property options (as documented above) to control various aspects of the home directory + and its user accounts. + + + + remove USER + + Remove a home directory/user account. + + + + update USER + update PATH USER + + Update a home directory/user account. Use the various user record property options + (as documented above) to make changes to the account, or alternatively provide a full, updated JSON + user record via the option. + + Note that changes to user records not signed by a cryptographic private key available locally + are not permitted, unless is used with a user record that is already + correctly signed by a recognized private key. + + + + passwd USER + + Change the password of the specified home direcory/user account. + + + + resize USER BYTES + + Change the disk space assigned to the specified home directory. If the LUKS2 storage + mechanism is used this will automatically resize the loopback file and the file system contained + within. Note that if ext4 is used inside of the LUKS2 volume the home directory + while activated may only be grown, and for being shrunk needs to be deactivated first (i.e. the user + has to log out). If xfs is used inside of the LUKS2 volume the home directory may + not be shrunk whatsoever. On all three of ext4, xfs and + btrfs the home directory may be grown while the user is logged in, and on the + latter also shrunk while the user is logged in. If the subvolume, + directory, fscrypt storage mechanisms are used, resizing will + change file system quota. + + + + lock USER + + Temporarily suspend access to the user's home directory and remove any associated + cryptographic keys from memory. Any attempts to access the user's home directory will stall until the + home directory is unlocked again (while re-authenticating). This functionality is primarily intended + to be used during system suspend to make sure the user's data cannot be accessed until the user + re-authenticates on resume. This operation is only defined for home directories that use the LUKS2 + storage mechanism. + + + + unlock USER + + Resume access to the user's home directory again, undoing the effect of + lock above. This requires authentication of the user, as the cryptographic keys + required for access to the home directory need to be reacquired. + + + + lock-all + + Execute the lock command on all suitable home directories at + once. This operation is generally execute on system suspend, to ensure all active user's + cryptographic keys for accessing their home directories are removed from memory. + + + + with USER COMMAND… + + Activate the specified user's home directory, run the specified command (under the + caller's identity, not the specified user's) and deactivate the home directory afterwards again + (unless the user is logged in otherwise). This command is useful for running privileged backup + scripts and such, but requires authentication with the user's credentials in order to be able to + unlock the user's home directory. + + + + + + Exit status + + On success, 0 is returned, a non-zero failure code otherwise. + + + + + + Examples + + + Create a user <literal>waldo</literal> in the administrator group <literal>wheel</literal>, and + assign 500 MiB disk space to them. + + homectl create waldo --real-name="Waldo McWaldo" -G wheel --disk-size=500M + + + + Create a user <literal>wally</literal> on a USB stick, and assign a maximum of 500 concurrent + tasks to them. + + homectl create wally --real-name="Wally McWally" --image-path=/dev/disk/by-id/usb-SanDisk_Ultra_Fit_476fff954b2b5c44-0:0 --tasks-max=500 + + + + Change nice level of user <literal>odlaw</literal> to +5 and make sure the environment variable + <varname>$SOME</varname> is set to the string <literal>THING</literal> for them on login. + + homectl update odlaw --nice=5 --setenv=SOME=THING + + + + + See Also + + systemd1, + systemd-homed.service8, + userdbctl1, + useradd8, + cryptsetup8 + + + + diff --git a/man/pam_systemd_home.xml b/man/pam_systemd_home.xml new file mode 100644 index 0000000000000..f0a58bda15cfb --- /dev/null +++ b/man/pam_systemd_home.xml @@ -0,0 +1,126 @@ + + + + + + + + pam_systemd_home + systemd + + + + pam_systemd_home + 8 + + + + pam_systemd_home + Automatically mount home directories managed by systemd-homed.service on logout, and unmount them on logout + + + + pam_systemd_home.so + + + + Description + + pam_systemd_home ensures that home directories managed by + systemd-homed.service8 + are automatically activated (mounted) on user login, and are deactivated (unmounted) when the last + session of the user ends. + + + + Options + + The following options are understood: + + + + + suspend= + + Takes a boolean argument. If true, the home directory of the user will be suspended + automatically during system suspend; if false it will remain active. Automatic suspending of the home + directory improves security substantially as secret key material is automatically removed from memory + before the system is put into sleep and must be re-acquired (by user re-authentication) when coming + back from suspend. It is recommended to set this parameter for all PAM applications that have support + for automatically re-authenticating via PAM on system resume. If this behaviour is enabled without + the PAM application supporting this, access to the home directory will sooner or later stall and will + resume only after the user re-authenticated via any PAM session. If multiple sessions of the same + user are open in parallel the user's home directory will be left unsuspended on system suspend as + soon as at least one of the sessions does not set this parameter. Defaults to off. + + + + debug= + + Takes an optional boolean argument. If yes or without the argument, the module will log + debugging information as it operates. + + + + + + Module Types Provided + + The module provides all four management operations: , , + , . + + + + Environment + + The following environment variables are initialized by the module and available to the processes of the + user's session: + + + + $SYSTEMD_HOME=1 + + Indicates that the user's home directory is managed by systemd-homed.service. + + + + + + + Example + + #%PAM-1.0 +auth sufficient pam_unix.so nullok try_first_pass +auth sufficient pam_systemd_home.so +auth required pam_deny.so + +account required pam_nologin.so +account sufficient pam_unix.so +account sufficient pam_systemd_home.so + +password sufficient pam_unix.so nullok sha512 shadow try_first_pass try_authtok +password sufficient pam_systemd_home.so +password required pam_deny.so + +-session optional pam_keyinit.so revoke +-session optional pam_loginuid.so +-session optional pam_systemd_home.so +-session optional pam_systemd.so +session required pam_unix.so + + + + See Also + + systemd1, + systemd-homed.service8, + homed.conf5, + homectl1, + pam.conf5, + pam.d5, + pam8 + + + + diff --git a/man/rules/meson.build b/man/rules/meson.build index 3b63311d7b52b..6efe2b6045c5f 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -17,6 +17,7 @@ manpages = [ ['environment.d', '5', [], 'ENABLE_ENVIRONMENT_D'], ['file-hierarchy', '7', [], ''], ['halt', '8', ['poweroff', 'reboot'], ''], + ['homectl', '1', [], 'ENABLE_HOMED'], ['hostname', '5', [], ''], ['hostnamectl', '1', [], 'ENABLE_HOSTNAMED'], ['hwdb', '7', [], 'ENABLE_HWDB'], @@ -45,6 +46,7 @@ manpages = [ ['nss-systemd', '8', ['libnss_systemd.so.2'], 'ENABLE_NSS_SYSTEMD'], ['os-release', '5', [], ''], ['pam_systemd', '8', [], 'HAVE_PAM'], + ['pam_systemd_home', '8', [], 'HAVE_PAM'], ['portablectl', '1', [], 'ENABLE_PORTABLED'], ['pstore.conf', '5', ['pstore.conf.d'], 'ENABLE_PSTORE'], ['resolvectl', '1', ['resolvconf'], 'ENABLE_RESOLVE'], @@ -271,6 +273,7 @@ manpages = [ ['sd_bus_message_read_array', '3', [], ''], ['sd_bus_message_read_basic', '3', [], ''], ['sd_bus_message_rewind', '3', [], ''], + ['sd_bus_message_sensitive', '3', [], ''], ['sd_bus_message_set_destination', '3', ['sd_bus_message_get_destination', @@ -697,6 +700,7 @@ manpages = [ '8', ['systemd-hibernate-resume'], 'ENABLE_HIBERNATE'], + ['systemd-homed.service', '8', ['systemd-homed'], 'ENABLE_HOMED'], ['systemd-hostnamed.service', '8', ['systemd-hostnamed'], 'ENABLE_HOSTNAMED'], ['systemd-hwdb', '8', [], 'ENABLE_HWDB'], ['systemd-id128', '1', [], ''], @@ -811,6 +815,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'], @@ -942,6 +947,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/sd_bus_message_sensitive.xml b/man/sd_bus_message_sensitive.xml new file mode 100644 index 0000000000000..a4a732cfd152c --- /dev/null +++ b/man/sd_bus_message_sensitive.xml @@ -0,0 +1,85 @@ + + + + + + + + sd_bus_message_sensitive + systemd + + + + sd_bus_message_sensitive + 3 + + + + sd_bus_message_sensitive + + Mark a message object as containing sensitive data + + + + + #include <systemd/sd-bus.h> + + + int sd_bus_message_sensitive + sd_bus_message *message + + + + + + Description + + sd_bus_message_sensitive() marks an allocated bus message as containing + sensitive data. This ensures that the message data is carefully removed from memory (specifically, + overwritten with zero bytes) when released. It is recommended to mark all incoming and outgoing messages + like this that contain security credentials and similar data that should be dealt with carefully. Note + that it is not possible to unmark messages like this, it's a one way operation. If a message is already + marked sensitive and then marked sensitive a second time the message remains marked so and no further + operation is executed. + + As a safety precaution all messages that are created as reply to messages that are marked sensitive + are also implicitly marked so. + + + + Return Value + + On success, theis functions return 0 or a positive integer. On failure, it returns a + negative errno-style error code. + + + Errors + + Returned errors may indicate the following problems: + + + + -EINVAL + + The message parameter is + NULL. + + + + + + + + + + See Also + + + systemd1, + sd-bus3, + sd_bus_message_new_method_call3 + + + + diff --git a/man/systemd-homed.service.xml b/man/systemd-homed.service.xml new file mode 100644 index 0000000000000..9313ea03141a5 --- /dev/null +++ b/man/systemd-homed.service.xml @@ -0,0 +1,57 @@ + + + + + + + + systemd-homed.service + systemd + + + + systemd-homed.service + 8 + + + + systemd-homed.service + systemd-homed + Home Directory/User Account Manager + + + + systemd-homed.service + /usr/lib/systemd/systemd-homed + + + + Description + + systemd-homed is a system service that may be used to create, remove, change or + inspect home directories. + + Most of systemd-homed's functionality is accessible through the + homectl1 command. + + See the Home Directories documentation for + details about the format and design of home directories managed by + systemd-homed.service. + + Each home directory managed by systemd-homed.service synthesizes a local user + and group. These are made available to the system using the Varlink User/Group Record Lookup API, and thus may be browsed + with + userdbctl1. + + + + See Also + + systemd1, + homectl1, + userdbctl1 + + + diff --git a/man/systemd-userdbd.service.xml b/man/systemd-userdbd.service.xml new file mode 100644 index 0000000000000..b73004f7d29f4 --- /dev/null +++ b/man/systemd-userdbd.service.xml @@ -0,0 +1,68 @@ + + + + + + + + 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 + Varlink User/Group Record Lookup API, 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 + + + diff --git a/man/userdbctl.xml b/man/userdbctl.xml new file mode 100644 index 0000000000000..72043a423aedb --- /dev/null +++ b/man/userdbctl.xml @@ -0,0 +1,233 @@ + + + + + + + + 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 advanced 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 Varlink User/Group Lookup API. + + + + Options + + The following options are understood: + + + + + MODE + + Choose the display 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 enquire for looking up or enumerating 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 enquired 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 display mode. + + + + group GROUP + + List all known group records or show details of one or more specified group + records. Use to tweak display 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 display + 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 display + 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. + + + + + + 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. + + + + + + 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 e5ceb1e169db3..1bdb6d7d891ee 100644 --- a/meson.build +++ b/meson.build @@ -242,6 +242,8 @@ 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_HOMEWORK_PATH', join_paths(rootlibexecdir, 'systemd-homework')) +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()) @@ -836,6 +838,12 @@ endif libmount = dependency('mount', version : fuzzer_build ? '>= 0' : '>= 2.30') +want_libfdisk = get_option('homed') +libfdisk = dependency('fdisk', required : want_libfdisk) + +libpwquality = dependency('pwquality') +conf.set10('HAVE_LIBPWQUALITY', libpwquality.found()) + want_seccomp = get_option('seccomp') if want_seccomp != 'false' and not skip_deps libseccomp = dependency('libseccomp', @@ -960,7 +968,7 @@ conf.set10('HAVE_MICROHTTPD', have) want_libcryptsetup = get_option('libcryptsetup') if want_libcryptsetup != 'false' and not skip_deps libcryptsetup = dependency('libcryptsetup', - version : '>= 1.6.0', + version : '>= 2.2.0', required : want_libcryptsetup == 'true') have = libcryptsetup.found() have_sector = cc.has_member( @@ -1073,6 +1081,17 @@ else endif conf.set10('HAVE_OPENSSL', have) +want_p11kit = get_option('p11kit') +if want_p11kit != 'false' and not skip_deps + libp11kit = dependency('p11-kit-1', + required : want_p11kit == 'true') + have = libp11kit.found() +else + have = false + libp11kit = [] +endif +conf.set10('HAVE_P11KIT', have) + want_elfutils = get_option('elfutils') if want_elfutils != 'false' and not skip_deps libdw = dependency('libdw', @@ -1279,6 +1298,8 @@ foreach term : ['utmp', 'localed', 'machined', 'portabled', + 'userdb', + 'homed', 'networkd', 'timedated', 'timesyncd', @@ -1492,6 +1513,8 @@ subdir('src/kernel-install') subdir('src/locale') subdir('src/machine') subdir('src/portable') +subdir('src/userdb') +subdir('src/home') subdir('src/nspawn') subdir('src/resolve') subdir('src/timedate') @@ -1518,7 +1541,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']] @@ -1529,9 +1552,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, @@ -1928,6 +1956,94 @@ 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 + +if conf.get('ENABLE_HOMED') == 1 + executable('systemd-homework', + systemd_homework_sources, + include_directories : includes, + link_with : [libshared], + dependencies : [threads, + libcryptsetup, + libblkid, + libcrypt, + libopenssl, + libfdisk], + install_rpath : rootlibexecdir, + install : true, + install_dir : rootlibexecdir) + + executable('systemd-homed', + systemd_homed_sources, + include_directories : includes, + link_with : [libshared], + dependencies : [threads, + libcrypt, + libopenssl, + libpwquality], + install_rpath : rootlibexecdir, + install : true, + install_dir : rootlibexecdir) + + executable('homectl', + homectl_sources, + include_directories : includes, + link_with : [libshared], + dependencies : [threads, + libcrypt, + libpwquality], + install_rpath : rootlibexecdir, + install : true, + install_dir : rootbindir) + + if conf.get('HAVE_PAM') == 1 + version_script_arg = join_paths(project_source_root, pam_systemd_home_sym) + pam_systemd = shared_library( + 'pam_systemd_home', + pam_systemd_home_c, + name_prefix : '', + include_directories : includes, + link_args : ['-shared', + '-Wl,--version-script=' + version_script_arg], + link_with : [libsystemd_static, + libshared_static], + dependencies : [threads, + libpam, + libpam_misc, + libcrypt], + link_depends : pam_systemd_home_sym, + install : true, + install_dir : pamlibdir) + endif +endif + foreach alias : ['halt', 'poweroff', 'reboot', 'runlevel', 'shutdown', 'telinit'] meson.add_install_script(meson_make_symlink, join_paths(rootbindir, 'systemctl'), @@ -1964,10 +2080,13 @@ executable('systemd-system-update-generator', if conf.get('HAVE_LIBCRYPTSETUP') == 1 executable('systemd-cryptsetup', - 'src/cryptsetup/cryptsetup.c', + ['src/cryptsetup/cryptsetup.c', + 'src/cryptsetup/cryptsetup-pkcs11.c', + 'src/cryptsetup/cryptsetup-pkcs11.h'], include_directories : includes, link_with : [libshared], - dependencies : [libcryptsetup], + dependencies : [libcryptsetup, + libp11kit], install_rpath : rootlibexecdir, install : true, install_dir : rootlibexecdir) diff --git a/meson_options.txt b/meson_options.txt index 5dc898eb80480..3c314adf0140e 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -90,6 +90,10 @@ 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('homed', type : 'boolean', + description : 'install the systemd-homed stack') option('networkd', type : 'boolean', description : 'install the systemd-networkd stack') option('timedated', type : 'boolean', @@ -278,6 +282,8 @@ option('gnutls', type : 'combo', choices : ['auto', 'true', 'false'], description : 'gnutls support') option('openssl', type : 'combo', choices : ['auto', 'true', 'false'], description : 'openssl support') +option('p11kit', type : 'combo', choices : ['auto', 'true', 'false'], + description : 'p11kit support') option('elfutils', type : 'combo', choices : ['auto', 'true', 'false'], description : 'elfutils support') option('zlib', type : 'combo', choices : ['auto', 'true', 'false'], diff --git a/src/basic/errno-util.h b/src/basic/errno-util.h index 6053cde62dd42..670772623b299 100644 --- a/src/basic/errno-util.h +++ b/src/basic/errno-util.h @@ -86,3 +86,26 @@ static inline bool ERRNO_IS_RESOURCE(int r) { ENFILE, ENOMEM); } + +/* Three different errors for "operation/system call/ioctl not supported" */ +static inline bool ERRNO_IS_NOT_SUPPORTED(int r) { + return IN_SET(abs(r), + EOPNOTSUPP, + ENOTTY, + ENOSYS); +} + +/* Three difference errors for "not enough disk space" */ +static inline bool ERRNO_IS_DISK_SPACE(int r) { + return IN_SET(abs(r), + ENOSPC, + EDQUOT, + EFBIG); +} + +/* Two different errors for access problems */ +static inline bool ERRNO_IS_PRIVILEGE(int r) { + return IN_SET(abs(r), + EACCES, + EPERM); +} diff --git a/src/basic/fileio.c b/src/basic/fileio.c index 623e43e4caeae..eb7e92f88d811 100644 --- a/src/basic/fileio.c +++ b/src/basic/fileio.c @@ -138,16 +138,21 @@ static int write_string_file_atomic( assert(fn); assert(line); + /* Note that we'd really like to use O_TMPFILE here, but can't really, since we want replacement + * semantics here, and O_TMPFILE can't offer that. i.e. rename() replaces but linkat() doesn't. */ + r = fopen_temporary(fn, &f, &p); if (r < 0) return r; - (void) fchmod_umask(fileno(f), 0644); - r = write_string_stream_ts(f, line, flags, ts); if (r < 0) goto fail; + r = fchmod_umask(fileno(f), FLAGS_SET(flags, WRITE_STRING_FILE_MODE_0600) ? 0600 : 0644); + if (r < 0) + goto fail; + if (rename(p, fn) < 0) { r = -errno; goto fail; @@ -167,7 +172,7 @@ int write_string_file_ts( struct timespec *ts) { _cleanup_fclose_ FILE *f = NULL; - int q, r; + int q, r, fd; assert(fn); assert(line); @@ -192,26 +197,20 @@ int write_string_file_ts( } else assert(!ts); - if (flags & WRITE_STRING_FILE_CREATE) { - r = fopen_unlocked(fn, "we", &f); - if (r < 0) - goto fail; - } else { - int fd; - - /* We manually build our own version of fopen(..., "we") that - * works without O_CREAT */ - fd = open(fn, O_WRONLY|O_CLOEXEC|O_NOCTTY | ((flags & WRITE_STRING_FILE_NOFOLLOW) ? O_NOFOLLOW : 0)); - if (fd < 0) { - r = -errno; - goto fail; - } + /* We manually build our own version of fopen(..., "we") that works without O_CREAT and with O_NOFOLLOW if needed. */ + fd = open(fn, O_WRONLY|O_CLOEXEC|O_NOCTTY | + (FLAGS_SET(flags, WRITE_STRING_FILE_NOFOLLOW) ? O_NOFOLLOW : 0) | + (FLAGS_SET(flags, WRITE_STRING_FILE_CREATE) ? O_CREAT : 0), + (FLAGS_SET(flags, WRITE_STRING_FILE_MODE_0600) ? 0600 : 0666)); + if (fd < 0) { + r = -errno; + goto fail; + } - r = fdopen_unlocked(fd, "w", &f); - if (r < 0) { - safe_close(fd); - goto fail; - } + r = fdopen_unlocked(fd, "w", &f); + if (r < 0) { + safe_close(fd); + goto fail; } if (flags & WRITE_STRING_FILE_DISABLE_BUFFER) @@ -438,17 +437,19 @@ int read_full_stream_full( return r; } -int read_full_file_full(const char *filename, ReadFullFileFlags flags, char **contents, size_t *size) { +int read_full_file_full(int dir_fd, const char *filename, ReadFullFileFlags flags, char **contents, size_t *size) { _cleanup_fclose_ FILE *f = NULL; int r; assert(filename); assert(contents); - r = fopen_unlocked(filename, "re", &f); + r = xfopenat(dir_fd, filename, "re", 0, &f); if (r < 0) return r; + (void) __fsetlocking(f, FSETLOCKING_BYCALLER); + return read_full_stream_full(f, filename, flags, contents, size); } @@ -575,6 +576,81 @@ DIR *xopendirat(int fd, const char *name, int flags) { return d; } +static int mode_to_flags(const char *mode) { + const char *p; + int flags; + + if ((p = startswith(mode, "r+"))) + flags = O_RDWR; + else if ((p = startswith(mode, "r"))) + flags = O_RDONLY; + else if ((p = startswith(mode, "w+"))) + flags = O_RDWR|O_CREAT|O_TRUNC; + else if ((p = startswith(mode, "w"))) + flags = O_WRONLY|O_CREAT|O_TRUNC; + else if ((p = startswith(mode, "a+"))) + flags = O_RDWR|O_CREAT|O_APPEND; + else if ((p = startswith(mode, "a"))) + flags = O_WRONLY|O_CREAT|O_APPEND; + else + return -EINVAL; + + for (; *p != 0; p++) { + + switch (*p) { + + case 'e': + flags |= O_CLOEXEC; + break; + + case 'x': + flags |= O_EXCL; + break; + + case 'm': + /* ignore this here, fdopen() might care later though */ + break; + + case 'c': /* not sure what to do about this one */ + default: + return -EINVAL; + } + } + + return flags; +} + +int xfopenat(int dir_fd, const char *path, const char *mode, int flags, FILE **ret) { + FILE *f; + + /* A combination of fopen() with openat() */ + + if (dir_fd == AT_FDCWD && flags == 0) { + f = fopen(path, mode); + if (!f) + return -errno; + } else { + int fd, mode_flags; + + mode_flags = mode_to_flags(mode); + if (mode_flags < 0) + return mode_flags; + + fd = openat(dir_fd, path, mode_flags | flags); + if (fd < 0) + return -errno; + + f = fdopen(fd, mode); + if (!f) { + safe_close(fd); + return -errno; + } + } + + *ret = f; + return 0; +} + static int search_and_fopen_internal(const char *path, const char *mode, const char *root, char **search, FILE **_f) { char **i; diff --git a/src/basic/fileio.h b/src/basic/fileio.h index 05f6c89da09dc..3cf1ab77bf6a3 100644 --- a/src/basic/fileio.h +++ b/src/basic/fileio.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "macro.h" @@ -22,6 +23,7 @@ typedef enum { WRITE_STRING_FILE_DISABLE_BUFFER = 1 << 5, WRITE_STRING_FILE_NOFOLLOW = 1 << 6, WRITE_STRING_FILE_MKDIR_0755 = 1 << 7, + WRITE_STRING_FILE_MODE_0600 = 1 << 8, /* And before you wonder, why write_string_file_atomic_label_ts() is a separate function instead of just one more flag here: it's about linking: we don't want to pull -lselinux into all users of write_string_file() @@ -52,9 +54,9 @@ static inline int write_string_file(const char *fn, const char *line, WriteStrin int write_string_filef(const char *fn, WriteStringFileFlags flags, const char *format, ...) _printf_(3, 4); int read_one_line_file(const char *filename, char **line); -int read_full_file_full(const char *filename, ReadFullFileFlags flags, char **contents, size_t *size); +int read_full_file_full(int dir_fd, const char *filename, ReadFullFileFlags flags, char **contents, size_t *size); static inline int read_full_file(const char *filename, char **contents, size_t *size) { - return read_full_file_full(filename, 0, contents, size); + return read_full_file_full(AT_FDCWD, filename, 0, contents, size); } int read_full_stream_full(FILE *f, const char *filename, ReadFullFileFlags flags, char **contents, size_t *size); static inline int read_full_stream(FILE *f, char **contents, size_t *size) { @@ -68,6 +70,7 @@ int executable_is_script(const char *path, char **interpreter); int get_proc_field(const char *filename, const char *pattern, const char *terminator, char **field); DIR *xopendirat(int dirfd, const char *name, int flags); +int xfopenat(int dir_fd, const char *path, const char *mode, int flags, FILE **ret); int search_and_fopen(const char *path, const char *mode, const char *root, const char **search, FILE **_f); int search_and_fopen_nulstr(const char *path, const char *mode, const char *root, const char *search, FILE **_f); diff --git a/src/basic/memory-util.h b/src/basic/memory-util.h index 9cb8ac3c10fca..66c64bdf0e779 100644 --- a/src/basic/memory-util.h +++ b/src/basic/memory-util.h @@ -79,14 +79,21 @@ static inline void* explicit_bzero_safe(void *p, size_t l) { void *explicit_bzero_safe(void *p, size_t l); #endif -static inline void erase_and_freep(void *p) { - void *ptr = *(void**) p; +static inline void* erase_and_free(void *p) { + size_t l; + + if (!p) + return NULL; + + l = malloc_usable_size(p); + explicit_bzero_safe(p, l); + free(p); - if (ptr) { - size_t l = malloc_usable_size(ptr); - explicit_bzero_safe(ptr, l); - free(ptr); - } + return NULL; +} + +static inline void erase_and_freep(void *p) { + erase_and_free(*(void**) p); } /* Use with _cleanup_ to erase a single 'char' when leaving scope */ diff --git a/src/basic/missing_magic.h b/src/basic/missing_magic.h index 4910cd368f90a..80bcfcca68edb 100644 --- a/src/basic/missing_magic.h +++ b/src/basic/missing_magic.h @@ -32,3 +32,8 @@ #ifndef MQUEUE_MAGIC #define MQUEUE_MAGIC 0x19800202 #endif + +/* Not exposed yet (4.20). Defined in fs/xfs/libxfs/xfs_format.h */ +#ifndef XFS_SB_MAGIC +#define XFS_SB_MAGIC 0x58465342 +#endif diff --git a/src/basic/missing_xfs.h b/src/basic/missing_xfs.h new file mode 100644 index 0000000000000..9eac76dd67c4e --- /dev/null +++ b/src/basic/missing_xfs.h @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +/* This is currently not exported in the public kernel headers, but the libxfs library code part of xfsprogs + * defines it as public header */ + +#ifndef XFS_IOC_FSGEOMETRY +#define XFS_IOC_FSGEOMETRY _IOR ('X', 124, struct xfs_fsop_geom) + +typedef struct xfs_fsop_geom { + uint32_t blocksize; + uint32_t rtextsize; + uint32_t agblocks; + uint32_t agcount; + uint32_t logblocks; + uint32_t sectsize; + uint32_t inodesize; + uint32_t imaxpct; + uint64_t datablocks; + uint64_t rtblocks; + uint64_t rtextents; + uint64_t logstart; + unsigned char uuid[16]; + uint32_t sunit; + uint32_t swidth; + int32_t version; + uint32_t flags; + uint32_t logsectsize; + uint32_t rtsectsize; + uint32_t dirblocksize; + uint32_t logsunit; +} xfs_fsop_geom_t; +#endif + +#ifndef XFS_IOC_FSGROWFSDATA +#define XFS_IOC_FSGROWFSDATA _IOW ('X', 110, struct xfs_growfs_data) + +typedef struct xfs_growfs_data { + uint64_t newblocks; + uint32_t imaxpct; +} xfs_growfs_data_t; +#endif diff --git a/src/basic/nss-util.h b/src/basic/nss-util.h index 2045175d1cb3c..29cf22676ae6f 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/ordered-set.h b/src/basic/ordered-set.h index ba43451e27d4a..383a729cab869 100644 --- a/src/basic/ordered-set.h +++ b/src/basic/ordered-set.h @@ -50,6 +50,10 @@ static inline void* ordered_set_remove(OrderedSet *s, void *p) { return ordered_hashmap_remove((OrderedHashmap*) s, p); } +static inline void* ordered_set_first(OrderedSet *s) { + return ordered_hashmap_first((OrderedHashmap*) s); +} + static inline void* ordered_set_steal_first(OrderedSet *s) { return ordered_hashmap_steal_first((OrderedHashmap*) s); } diff --git a/src/basic/process-util.c b/src/basic/process-util.c index 5452edd7a4bc0..f8822a8309952 100644 --- a/src/basic/process-util.c +++ b/src/basic/process-util.c @@ -1319,6 +1319,13 @@ int safe_fork_full( log_full_errno(prio, r, "Failed to connect stdin/stdout to /dev/null: %m"); _exit(EXIT_FAILURE); } + + } else if (flags & FORK_STDOUT_TO_STDERR) { + + if (dup2(STDERR_FILENO, STDOUT_FILENO) < 0) { + log_full_errno(prio, r, "Failed to connect stdout to stderr: %m"); + _exit(EXIT_FAILURE); + } } if (flags & FORK_RLIMIT_NOFILE_SAFE) { diff --git a/src/basic/process-util.h b/src/basic/process-util.h index 41d4759c97136..dbe216cd9f100 100644 --- a/src/basic/process-util.h +++ b/src/basic/process-util.h @@ -148,16 +148,17 @@ void reset_cached_pid(void); int must_be_root(void); typedef enum ForkFlags { - FORK_RESET_SIGNALS = 1 << 0, /* Reset all signal handlers and signal mask */ - FORK_CLOSE_ALL_FDS = 1 << 1, /* Close all open file descriptors in the child, except for 0,1,2 */ - FORK_DEATHSIG = 1 << 2, /* Set PR_DEATHSIG in the child */ - FORK_NULL_STDIO = 1 << 3, /* Connect 0,1,2 to /dev/null */ - FORK_REOPEN_LOG = 1 << 4, /* Reopen log connection */ - FORK_LOG = 1 << 5, /* Log above LOG_DEBUG log level about failures */ - FORK_WAIT = 1 << 6, /* Wait until child exited */ - FORK_NEW_MOUNTNS = 1 << 7, /* Run child in its own mount namespace */ - FORK_MOUNTNS_SLAVE = 1 << 8, /* Make child's mount namespace MS_SLAVE */ - FORK_RLIMIT_NOFILE_SAFE = 1 << 9, /* Set RLIMIT_NOFILE soft limit to 1K for select() compat */ + FORK_RESET_SIGNALS = 1 << 0, /* Reset all signal handlers and signal mask */ + FORK_CLOSE_ALL_FDS = 1 << 1, /* Close all open file descriptors in the child, except for 0,1,2 */ + FORK_DEATHSIG = 1 << 2, /* Set PR_DEATHSIG in the child */ + FORK_NULL_STDIO = 1 << 3, /* Connect 0,1,2 to /dev/null */ + FORK_REOPEN_LOG = 1 << 4, /* Reopen log connection */ + FORK_LOG = 1 << 5, /* Log above LOG_DEBUG log level about failures */ + FORK_WAIT = 1 << 6, /* Wait until child exited */ + FORK_NEW_MOUNTNS = 1 << 7, /* Run child in its own mount namespace */ + FORK_MOUNTNS_SLAVE = 1 << 8, /* Make child's mount namespace MS_SLAVE */ + FORK_RLIMIT_NOFILE_SAFE = 1 << 9, /* Set RLIMIT_NOFILE soft limit to 1K for select() compat */ + FORK_STDOUT_TO_STDERR = 1 << 10, /* Make stdout a copy of stderr */ } ForkFlags; int safe_fork_full(const char *name, const int except_fds[], size_t n_except_fds, ForkFlags flags, pid_t *ret_pid); diff --git a/src/basic/string-util.c b/src/basic/string-util.c index 9586b3940eb93..418a50e2bc7e3 100644 --- a/src/basic/string-util.c +++ b/src/basic/string-util.c @@ -1050,3 +1050,13 @@ bool string_is_safe(const char *p) { return true; } + +char* string_erase(char *x) { + if (!x) + return NULL; + + /* A delicious drop of snake-oil! To be called on memory where we stored passphrases or so, after we + * used them. */ + explicit_bzero_safe(x, strlen(x)); + return x; +} diff --git a/src/basic/string-util.h b/src/basic/string-util.h index 76767afcac49a..ddbb87e1fcf3e 100644 --- a/src/basic/string-util.h +++ b/src/basic/string-util.h @@ -263,3 +263,5 @@ static inline char* str_realloc(char **p) { return (*p = t); } + +char* string_erase(char *x); diff --git a/src/basic/tmpfile-util.c b/src/basic/tmpfile-util.c index afcf58aeac8bb..2b9ce5ef7a8af 100644 --- a/src/basic/tmpfile-util.c +++ b/src/basic/tmpfile-util.c @@ -20,50 +20,60 @@ #include "tmpfile-util.h" #include "umask-util.h" -int fopen_temporary(const char *path, FILE **_f, char **_temp_path) { - FILE *f; - char *t; - int r, fd; +int fopen_temporary(const char *path, FILE **ret_f, char **ret_temp_path) { + _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char *t = NULL; + _cleanup_close_ int fd = -1; + int r; - assert(path); - assert(_f); - assert(_temp_path); + if (path) { + r = tempfn_xxxxxx(path, NULL, &t); + if (r < 0) + return r; + } else { + const char *d; - r = tempfn_xxxxxx(path, NULL, &t); - if (r < 0) - return r; + r = tmp_dir(&d); + if (r < 0) + return r; + + t = path_join(d, "XXXXXX"); + if (!t) + return -ENOMEM; + } fd = mkostemp_safe(t); - if (fd < 0) { - free(t); + if (fd < 0) return -errno; - } /* This assumes that returned FILE object is short-lived and used within the same single-threaded * context and never shared externally, hence locking is not necessary. */ r = fdopen_unlocked(fd, "w", &f); if (r < 0) { - unlink(t); - free(t); - safe_close(fd); + (void) unlink(t); return r; } - *_f = f; - *_temp_path = t; + TAKE_FD(fd); + + if (ret_f) + *ret_f = TAKE_PTR(f); + + if (ret_temp_path) + *ret_temp_path = TAKE_PTR(t); return 0; } /* This is much like mkostemp() but is subject to umask(). */ int mkostemp_safe(char *pattern) { - _unused_ _cleanup_umask_ mode_t u = umask(0077); int fd; assert(pattern); - fd = mkostemp(pattern, O_CLOEXEC); + RUN_WITH_UMASK(0077) + fd = mkostemp(pattern, O_CLOEXEC); if (fd < 0) return -errno; diff --git a/src/basic/user-util.c b/src/basic/user-util.c index 3b253bc264dd3..70355554afb62 100644 --- a/src/basic/user-util.c +++ b/src/basic/user-util.c @@ -89,7 +89,7 @@ char *getusername_malloc(void) { return uid_to_name(getuid()); } -static bool is_nologin_shell(const char *shell) { +bool is_nologin_shell(const char *shell) { return PATH_IN_SET(shell, /* 'nologin' is the friendliest way to disable logins for a user account. It prints a nice @@ -925,3 +925,15 @@ int make_salt(char **ret) { *ret = salt; return 0; } + +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 (!s) + return false; + + return s[0] == '$' || + (s[0] == '!' && s[1] == '$'); +} diff --git a/src/basic/user-util.h b/src/basic/user-util.h index cfa515f5e8a26..87972bdca4a1e 100644 --- a/src/basic/user-util.h +++ b/src/basic/user-util.h @@ -57,6 +57,14 @@ int take_etc_passwd_lock(const char *root); #define ETC_PASSWD_LOCK_PATH "/etc/.pwd.lock" +static inline bool uid_is_system(uid_t uid) { + return uid <= SYSTEM_UID_MAX; +} + +static inline bool gid_is_system(gid_t gid) { + return gid <= SYSTEM_GID_MAX; +} + static inline bool uid_is_dynamic(uid_t uid) { return DYNAMIC_UID_MIN <= uid && uid <= DYNAMIC_UID_MAX; } @@ -65,12 +73,12 @@ static inline bool gid_is_dynamic(gid_t gid) { return uid_is_dynamic((uid_t) gid); } -static inline bool uid_is_system(uid_t uid) { - return uid <= SYSTEM_UID_MAX; +static inline bool uid_is_container(uid_t uid) { + return CONTAINER_UID_BASE_MIN <= uid && uid <= CONTAINER_UID_BASE_MAX; } -static inline bool gid_is_system(gid_t gid) { - return gid <= SYSTEM_GID_MAX; +static inline bool gid_is_container(gid_t gid) { + return uid_is_container((uid_t) gid); } /* The following macros add 1 when converting things, since UID 0 is a valid UID, while the pointer @@ -127,3 +135,7 @@ int putsgent_sane(const struct sgrp *sg, FILE *stream); #endif int make_salt(char **ret); + +bool is_nologin_shell(const char *shell); + +bool hashed_password_valid(const char *s); diff --git a/src/core/core-varlink.c b/src/core/core-varlink.c new file mode 100644 index 0000000000000..628b9ccaaa0dc --- /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 0000000000000..89818e2766caa --- /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 e7a2f64525165..5dd7de2c7c4cd 100644 --- a/src/core/dynamic-user.c +++ b/src/core/dynamic-user.c @@ -531,7 +531,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. */ @@ -547,7 +546,9 @@ int dynamic_user_current(DynamicUser *d, uid_t *ret) { if (r < 0) return r; - *ret = uid; + if (ret) + *ret = uid; + return 0; } @@ -734,7 +735,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 d9114bb0c597a..ded90f3d32787 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -31,6 +31,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" @@ -45,8 +46,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" @@ -1337,6 +1338,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); @@ -1698,6 +1700,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 watchdog server, ignoring: %m"); + /* Third, fire things up! */ manager_coldplug(m); diff --git a/src/core/manager.h b/src/core/manager.h index 308ee013bda9d..73c68488a3ccf 100644 --- a/src/core/manager.h +++ b/src/core/manager.h @@ -15,6 +15,7 @@ #include "list.h" #include "prioq.h" #include "ratelimit.h" +#include "varlink.h" struct libmnt_monitor; typedef struct Unit Unit; @@ -423,6 +424,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 fb6820e109a9e..3586838f59b3b 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -22,8 +22,8 @@ libcore_sources = ''' bpf-firewall.h cgroup.c cgroup.h - chown-recursive.c - chown-recursive.h + core-varlink.c + core-varlink.h dbus-automount.c dbus-automount.h dbus-cgroup.c diff --git a/src/core/namespace.c b/src/core/namespace.c index 973b64007cf87..89b776f175dd7 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ #include +#include #include #include #include @@ -1215,6 +1216,7 @@ int setup_namespace( r = loop_device_make_by_path(root_image, dissect_image_flags & DISSECT_IMAGE_READ_ONLY ? O_RDONLY : O_RDWR, + LO_FLAGS_PARTSCAN, &loop_device); if (r < 0) return log_debug_errno(r, "Failed to create loop device for root image: %m"); diff --git a/src/cryptsetup/cryptsetup-pkcs11.c b/src/cryptsetup/cryptsetup-pkcs11.c new file mode 100644 index 0000000000000..a13e5427f2dee --- /dev/null +++ b/src/cryptsetup/cryptsetup-pkcs11.c @@ -0,0 +1,707 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include +#include + +#include +#include + +#include "alloc-util.h" +#include "ask-password-api.h" +#include "cryptsetup-pkcs11.h" +#include "escape.h" +#include "fd-util.h" +#include "macro.h" +#include "memory-util.h" +#include "stat-util.h" +#include "strv.h" + +DEFINE_TRIVIAL_CLEANUP_FUNC(P11KitUri*, p11_kit_uri_free); +DEFINE_TRIVIAL_CLEANUP_FUNC(CK_FUNCTION_LIST**, p11_kit_modules_finalize_and_release); + +static int token_login( + const char *friendly_name, + CK_FUNCTION_LIST *m, + CK_SESSION_HANDLE session, + CK_SLOT_ID slotid, + const CK_TOKEN_INFO *token_info, + const char *token_uri_string, + const char *token_label, + usec_t until) { + + _cleanup_free_ char *token_uri_escaped = NULL, *id = NULL; + CK_TOKEN_INFO updated_token_info; + CK_RV rv; + int r; + + assert(friendly_name); + assert(m); + assert(token_info); + assert(token_uri_string); + assert(token_label); + + if (FLAGS_SET(token_info->flags, CKF_PROTECTED_AUTHENTICATION_PATH)) { + rv = m->C_Login(session, CKU_USER, NULL, 0); + if (rv != CKR_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to log into security token '%s': %s", token_label, p11_kit_strerror(rv)); + + log_info("Successully logged into security token '%s' via protected authentication path.", token_label); + return 0; + } + + if (!FLAGS_SET(token_info->flags, CKF_LOGIN_REQUIRED)) { + log_info("No login into security token '%s' required.", token_label); + return 0; + } + + token_uri_escaped = cescape(token_uri_string); + if (!token_uri_escaped) + return log_oom(); + + id = strjoin("pkcs11:", token_uri_escaped); + if (!id) + return log_oom(); + + for (unsigned tries = 0; tries < 3; tries++) { + _cleanup_strv_free_erase_ char **passwords = NULL; + _cleanup_free_ char *text = NULL; + char **i; + + if (FLAGS_SET(token_info->flags, CKF_USER_PIN_FINAL_TRY)) + r = asprintf(&text, "Please enter correct PIN for security token '%s' in order to unlock disk %s (final try):", token_label, friendly_name); + if (FLAGS_SET(token_info->flags, CKF_USER_PIN_COUNT_LOW)) + r = asprintf(&text, "PIN has been entered incorrectly previously, please enter correct PIN for security token '%s' in order to unlock disk %s:", token_label, friendly_name); + else if (tries == 0) + r = asprintf(&text, "Please enter PIN for security token '%s' in order to unlock disk %s:", token_label, friendly_name); + else + r = asprintf(&text, "Please enter PIN for security token '%s' in order to unlock disk %s (try #%u):", token_label, friendly_name, tries+1); + if (r < 0) + return log_oom(); + + /* We never cache PINs, simply because it's fatal if we use wrong PINs, since usually there are only 3 tries */ + r = ask_password_auto(text, "drive-harddisk", id, "pkcs11-pin", until, 0, &passwords); + if (r < 0) + return log_error_errno(r, "Failed to query PIN for security token '%s': %m", token_label); + + STRV_FOREACH(i, passwords) { + rv = m->C_Login(session, CKU_USER, (CK_UTF8CHAR*) *i, strlen(*i)); + if (rv == CKR_OK) { + log_info("Successfully logged into security token '%s'.", token_label); + return 0; + } + if (rv == CKR_PIN_LOCKED) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "PIN has been locked, please reset PIN of security token '%s'.", token_label); + if (!IN_SET(rv, CKR_PIN_INCORRECT, CKR_PIN_LEN_RANGE)) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to log into security token '%s': %s", token_label, p11_kit_strerror(rv)); + + /* Referesh the token info, so that we can prompt knowing the new flags if they changed. */ + rv = m->C_GetTokenInfo(slotid, &updated_token_info); + if (rv != CKR_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to acquire updated security token information for slot %lu: %s", slotid, p11_kit_strerror(rv)); + + token_info = &updated_token_info; + log_notice("PIN for token '%s' is incorrect, please try again.", token_label); + } + } + + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Too many attempts to log into token '%s'.", token_label); +} + +static int token_find_private_key( + CK_FUNCTION_LIST *m, + CK_SESSION_HANDLE session, + P11KitUri *search_uri, + CK_OBJECT_HANDLE *ret_object) { + + bool found_decrypt = false, found_class = false, found_key_type = false; + _cleanup_free_ CK_ATTRIBUTE *attributes_buffer = NULL; + CK_ULONG n_attributes, a, n_objects; + CK_ATTRIBUTE *attributes = NULL; + CK_OBJECT_HANDLE objects[2]; + CK_RV rv, rv2; + + assert(m); + assert(search_uri); + assert(ret_object); + + attributes = p11_kit_uri_get_attributes(search_uri, &n_attributes); + for (a = 0; a < n_attributes; a++) { + + /* We use the URI's included match attributes, but make them more strict. This allows users + * to specify a token URL instead of an object URL and the right thing should happen if + * there's only one suitable key on the token. */ + + switch (attributes[a].type) { + + case CKA_CLASS: { + CK_OBJECT_CLASS c; + + if (attributes[a].ulValueLen != sizeof(c)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid PKCS#11 CKA_CLASS attribute size."); + + memcpy(&c, attributes[a].pValue, sizeof(c)); + if (c != CKO_PRIVATE_KEY) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected PKCS#11 object is not a private key, refusing."); + + found_class = true; + break; + } + + case CKA_DECRYPT: { + CK_BBOOL b; + + if (attributes[a].ulValueLen != sizeof(b)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid PKCS#11 CKA_DECRYPT attribute size."); + + memcpy(&b, attributes[a].pValue, sizeof(b)); + if (!b) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected PKCS#11 object is not suitable for decryption, refusing."); + + found_decrypt = true; + break; + } + + case CKA_KEY_TYPE: { + CK_KEY_TYPE t; + + if (attributes[a].ulValueLen != sizeof(t)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid PKCS#11 CKA_KEY_TYPE attribute size."); + + memcpy(&t, attributes[a].pValue, sizeof(t)); + if (t != CKK_RSA) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Selected PKCS#11 object is not an RSA key, refusing."); + + found_key_type = true; + break; + }} + } + + if (!found_decrypt || !found_class || !found_key_type) { + /* Hmm, let's slightly extend the attribute list we search for */ + + attributes_buffer = new(CK_ATTRIBUTE, n_attributes + !found_decrypt + !found_class + !found_key_type); + if (!attributes_buffer) + return log_oom(); + + memcpy(attributes_buffer, attributes, sizeof(CK_ATTRIBUTE) * n_attributes); + + if (!found_decrypt) { + static const CK_BBOOL yes = true; + + attributes_buffer[n_attributes++] = (CK_ATTRIBUTE) { + .type = CKA_DECRYPT, + .pValue = (CK_BBOOL*) &yes, + .ulValueLen = sizeof(yes), + }; + } + + if (!found_class) { + static const CK_OBJECT_CLASS class = CKO_PRIVATE_KEY; + + attributes_buffer[n_attributes++] = (CK_ATTRIBUTE) { + .type = CKA_CLASS, + .pValue = (CK_OBJECT_CLASS*) &class, + .ulValueLen = sizeof(class), + }; + } + + if (!found_key_type) { + static const CK_KEY_TYPE type = CKK_RSA; + + attributes_buffer[n_attributes++] = (CK_ATTRIBUTE) { + .type = CKA_KEY_TYPE, + .pValue = (CK_KEY_TYPE*) &type, + .ulValueLen = sizeof(type), + }; + } + + attributes = attributes_buffer; + } + + rv = m->C_FindObjectsInit(session, attributes, n_attributes); + if (rv != CKR_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to initialize object find call: %s", p11_kit_strerror(rv)); + + rv = m->C_FindObjects(session, objects, ELEMENTSOF(objects), &n_objects); + rv2 = m->C_FindObjectsFinal(session); + if (rv != CKR_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to search objects: %s", p11_kit_strerror(rv)); + if (rv2 != CKR_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to finalize object find call: %s", p11_kit_strerror(rv)); + if (n_objects == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Failed to find selected private key suitable for decryption on token."); + if (n_objects > 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Configured private key URI matches multiple keys, refusing."); + + *ret_object = objects[0]; + return 0; +} + +static int token_decrypt_our_key( + CK_FUNCTION_LIST *m, + CK_SESSION_HANDLE session, + CK_OBJECT_HANDLE object, + const char *token_label, + const void *encrypted_key, + size_t encrypted_key_size, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size) { + + static const CK_MECHANISM mechanism = { + .mechanism = CKM_RSA_PKCS + }; + _cleanup_(erase_and_freep) CK_BYTE *dbuffer = NULL; + CK_ULONG dbuffer_size = 0; + CK_RV rv; + + assert(m); + assert(encrypted_key); + assert(encrypted_key_size > 0); + assert(ret_decrypted_key); + assert(ret_decrypted_key_size); + + rv = m->C_DecryptInit(session, (CK_MECHANISM*) &mechanism, object); + if (rv != CKR_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to initialize decryption on security token '%s': %s", token_label, p11_kit_strerror(rv)); + + dbuffer_size = encrypted_key_size; /* Start with something reasonable */ + dbuffer = malloc(dbuffer_size); + if (!dbuffer) + return log_oom(); + + rv = m->C_Decrypt(session, (CK_BYTE*) encrypted_key, encrypted_key_size, dbuffer, &dbuffer_size); + if (rv == CKR_BUFFER_TOO_SMALL) { + erase_and_free(dbuffer); + + dbuffer = malloc(dbuffer_size); + if (!dbuffer) + return log_oom(); + + rv = m->C_Decrypt(session, (CK_BYTE*) encrypted_key, encrypted_key_size, dbuffer, &dbuffer_size); + } + if (rv != CKR_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to decrypt key on security token '%s': %s", token_label, p11_kit_strerror(rv)); + + log_info("Successfully decrypted key with security token '%s'.", token_label); + + *ret_decrypted_key = TAKE_PTR(dbuffer); + *ret_decrypted_key_size = dbuffer_size; + return 0; +} + +static int token_process( + const char *friendly_name, + CK_FUNCTION_LIST *m, + CK_SLOT_ID slotid, + const CK_TOKEN_INFO *token_info, + const char *token_uri_string, + P11KitUri *search_uri, + const void *encrypted_key, + size_t encrypted_key_size, + usec_t until, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size) { + + _cleanup_free_ char *token_label = NULL; + CK_SESSION_HANDLE session; + CK_OBJECT_HANDLE object; + CK_RV rv; + int r; + + assert(friendly_name); + assert(m); + assert(token_info); + assert(token_uri_string); + assert(search_uri); + assert(encrypted_key); + assert(encrypted_key_size > 0); + assert(ret_decrypted_key); + assert(ret_decrypted_key_size); + + /* The label is not NUL terminated and likely padded with spaces, let's make a copy here, so that we can strip that. */ + token_label = strndup((char*) token_info->label, sizeof(token_info->label)); + if (!token_label) + return log_oom(); + + strstrip(token_label); + + rv = m->C_OpenSession(slotid, CKF_SERIAL_SESSION, NULL, NULL, &session); + if (rv != CKR_OK) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to create session for security token '%s': %s", token_label, p11_kit_strerror(rv)); + + r = token_login(friendly_name, m, session, slotid, token_info, token_uri_string, token_label, until); + if (r < 0) + goto finish; + + r = token_find_private_key(m, session, search_uri, &object); + if (r < 0) + goto finish; + + r = token_decrypt_our_key(m, session, object, token_label, encrypted_key, encrypted_key_size, ret_decrypted_key, ret_decrypted_key_size); + if (r < 0) + goto finish; + + r = 1; + +finish: + rv = m->C_CloseSession(session); + if (rv != CKR_OK) + log_warning_errno(SYNTHETIC_ERRNO(rv), "Failed to close session on PKCS#11 token, ignoring: %s", p11_kit_strerror(rv)); + + return r; +} + +static int uri_from_string(const char *p, P11KitUri **ret) { + _cleanup_(p11_kit_uri_freep) P11KitUri *uri = NULL; + + assert(p); + assert(ret); + + uri = p11_kit_uri_new(); + if (!uri) + return -ENOMEM; + + if (p11_kit_uri_parse(p, P11_KIT_URI_FOR_ANY, uri) != P11_KIT_URI_OK) + return -EINVAL; + + *ret = TAKE_PTR(uri); + return 0; +} + +static P11KitUri *uri_from_module_info(const CK_INFO *info) { + P11KitUri *uri; + + assert(info); + + uri = p11_kit_uri_new(); + if (!uri) + return NULL; + + *p11_kit_uri_get_module_info(uri) = *info; + return uri; +} + +static P11KitUri *uri_from_slot_info(const CK_SLOT_INFO *slot_info) { + P11KitUri *uri; + + assert(slot_info); + + uri = p11_kit_uri_new(); + if (!uri) + return NULL; + + *p11_kit_uri_get_slot_info(uri) = *slot_info; + return uri; +} + +static P11KitUri *uri_from_token_info(const CK_TOKEN_INFO *token_info) { + P11KitUri *uri; + + assert(token_info); + + uri = p11_kit_uri_new(); + if (!uri) + return NULL; + + *p11_kit_uri_get_token_info(uri) = *token_info; + return uri; +} + +static int slot_process( + const char *friendly_name, + CK_FUNCTION_LIST *m, + CK_SLOT_ID slotid, + P11KitUri *search_uri, + const void *encrypted_key, + size_t encrypted_key_size, + usec_t until, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size) { + + _cleanup_(p11_kit_uri_freep) P11KitUri* slot_uri = NULL, *token_uri = NULL; + _cleanup_free_ char *token_uri_string = NULL; + CK_TOKEN_INFO token_info; + CK_SLOT_INFO slot_info; + int uri_result; + CK_RV rv; + + assert(friendly_name); + assert(m); + assert(search_uri); + assert(encrypted_key); + assert(encrypted_key_size > 0); + assert(ret_decrypted_key); + assert(ret_decrypted_key_size); + + /* We return -EAGAIN for all failures we can attribute to a specific slot in some way, so that the + * caller might try other slots before giving up. */ + + rv = m->C_GetSlotInfo(slotid, &slot_info); + if (rv != CKR_OK) { + log_warning("Failed to acquire slot info for slot %lu, ignoring slot: %s", slotid, p11_kit_strerror(rv)); + return -EAGAIN; + } + + slot_uri = uri_from_slot_info(&slot_info); + if (!slot_uri) + return log_oom(); + + if (DEBUG_LOGGING) { + _cleanup_free_ char *slot_uri_string = NULL; + + uri_result = p11_kit_uri_format(slot_uri, P11_KIT_URI_FOR_ANY, &slot_uri_string); + if (uri_result != P11_KIT_URI_OK) { + log_warning("Failed to format slot URI, ignoring slot: %s", p11_kit_uri_message(uri_result)); + return -EAGAIN; + } + + log_debug("Found slot with URI %s", slot_uri_string); + } + + rv = m->C_GetTokenInfo(slotid, &token_info); + if (rv == CKR_TOKEN_NOT_PRESENT) { + log_debug("Token not present in slot, ignoring."); + return -EAGAIN; + } else if (rv != CKR_OK) { + log_warning("Failed to acquire token info for slot %lu, ignoring slot: %s", slotid, p11_kit_strerror(rv)); + return -EAGAIN; + } + + token_uri = uri_from_token_info(&token_info); + if (!token_uri) + return log_oom(); + + uri_result = p11_kit_uri_format(token_uri, P11_KIT_URI_FOR_ANY, &token_uri_string); + if (uri_result != P11_KIT_URI_OK) { + log_warning("Failed to format slot URI: %s", p11_kit_uri_message(uri_result)); + return -EAGAIN; + } + + if (!p11_kit_uri_match_token_info(search_uri, &token_info)) { + log_debug("Found non-matching token with URI %s.", token_uri_string); + return -EAGAIN; + } + + log_debug("Found matching token with URI %s.", token_uri_string); + + return token_process( + friendly_name, + m, + slotid, + &token_info, + token_uri_string, + search_uri, + encrypted_key, encrypted_key_size, + until, + ret_decrypted_key, ret_decrypted_key_size); +} + +static int module_process( + const char *friendly_name, + CK_FUNCTION_LIST *m, + P11KitUri *search_uri, + const void *encrypted_key, + size_t encrypted_key_size, + usec_t until, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size) { + + _cleanup_free_ char *name = NULL, *module_uri_string = NULL; + _cleanup_(p11_kit_uri_freep) P11KitUri* module_uri = NULL; + _cleanup_free_ CK_SLOT_ID *slotids = NULL; + CK_ULONG n_slotids = 0; + int uri_result; + CK_INFO info; + size_t k; + CK_RV rv; + int r; + + /* We ignore most errors from modules here, in order to skip over faulty modules: one faulty module + * should not have the effect that we don't try the others anymore. We indicate such per-module + * failures with -EAGAIN, which let's the caller try the next module. */ + + name = p11_kit_module_get_name(m); + if (!name) + return log_oom(); + + log_debug("Trying PKCS#11 module %s.", name); + + rv = m->C_GetInfo(&info); + if (rv != CKR_OK) { + log_warning("Failed to get info on PKCS#11 module, ignoring module: %s", p11_kit_strerror(rv)); + return -EAGAIN; + } + + module_uri = uri_from_module_info(&info); + if (!module_uri) + return log_oom(); + + uri_result = p11_kit_uri_format(module_uri, P11_KIT_URI_FOR_ANY, &module_uri_string); + if (uri_result != P11_KIT_URI_OK) { + log_warning("Failed to format module URI, ignoring module: %s", p11_kit_uri_message(uri_result)); + return -EAGAIN; + } + + log_debug("Found module with URI %s", module_uri_string); + + for (unsigned tries = 0; tries < 16; tries++) { + slotids = mfree(slotids); + n_slotids = 0; + + rv = m->C_GetSlotList(0, NULL, &n_slotids); + if (rv != CKR_OK) { + log_warning("Failed to get slot list size, ignoring module: %s", p11_kit_strerror(rv)); + n_slotids = 0; + break; + } + if (n_slotids == 0) { + log_debug("This module has no slots? Ignoring module."); + break; + } + + slotids = new(CK_SLOT_ID, n_slotids); + if (!slotids) + return log_oom(); + + rv = m->C_GetSlotList(0, slotids, &n_slotids); + if (rv == CKR_OK) + break; + n_slotids = 0; + if (rv != CKR_BUFFER_TOO_SMALL) { + log_warning("Failed to acquire slot list, ignoring module: %s", p11_kit_strerror(rv)); + break; + } + + /* Hu? Maybe somebody plugged something in and things changed? Let's try again */ + } + + if (n_slotids == 0) + return -EAGAIN; + + for (k = 0; k < n_slotids; k++) { + r = slot_process( + friendly_name, + m, + slotids[k], + search_uri, + encrypted_key, encrypted_key_size, + until, + ret_decrypted_key, ret_decrypted_key_size); + if (r != -EAGAIN) + return r; + } + + return -EAGAIN; +} + +static int load_key_file( + const char *key_file, + size_t key_file_size, + uint64_t key_file_offset, + void **ret_encrypted_key, + size_t *ret_encrypted_key_size) { + + _cleanup_(erase_and_freep) char *buffer = NULL; + _cleanup_close_ int fd = -1; + ssize_t n; + int r; + + assert(key_file); + assert(ret_encrypted_key); + assert(ret_encrypted_key_size); + + fd = open(key_file, O_RDONLY|O_CLOEXEC); + if (fd < 0) + return log_error_errno(errno, "Failed to load encrypted PKCS#11 key: %m"); + + if (key_file_size == 0) { + struct stat st; + + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to stat key file: %m"); + + r = stat_verify_regular(&st); + if (r < 0) + return log_error_errno(r, "Key file is not a regular file: %m"); + + if (st.st_size == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Key file is empty, refusing."); + if ((uint64_t) st.st_size > SIZE_MAX) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Key file too large, refsing."); + + if (key_file_offset >= (uint64_t) st.st_size) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Key file offset too large for file, refusing."); + + key_file_size = st.st_size - key_file_offset; + } + + buffer = malloc(key_file_size); + if (!buffer) + return log_oom(); + + if (key_file_offset > 0) + n = pread(fd, buffer, key_file_size, key_file_offset); + else + n = read(fd, buffer, key_file_size); + if (n < 0) + return log_error_errno(errno, "Failed to read PKCS#11 key file: %m"); + if (n == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Empty encrypted key found, refusing."); + + *ret_encrypted_key = TAKE_PTR(buffer); + *ret_encrypted_key_size = (size_t) n; + + return 0; +} + +int acquire_pkcs11_key( + const char *friendly_name, + const char *pkcs11_uri, + const char *key_file, + size_t key_file_size, + uint64_t key_file_offset, + usec_t until, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size) { + + _cleanup_(p11_kit_modules_finalize_and_releasep) CK_FUNCTION_LIST **modules = NULL; + _cleanup_(p11_kit_uri_freep) P11KitUri *search_uri = NULL; + _cleanup_(erase_and_freep) void *encrypted_key = NULL; + size_t encrypted_key_size; + int r; + + assert(friendly_name); + assert(pkcs11_uri); + assert(key_file); + assert(ret_decrypted_key); + assert(ret_decrypted_key_size); + + r = load_key_file(key_file, key_file_size, key_file_offset, &encrypted_key, &encrypted_key_size); + if (r < 0) + return r; + + r = uri_from_string(pkcs11_uri, &search_uri); + if (r < 0) + return log_error_errno(r, "Failed to parse PKCS#11 URI '%s': %m", pkcs11_uri); + + modules = p11_kit_modules_load_and_initialize(0); + if (!modules) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to initialize pkcs11 modules"); + + for (CK_FUNCTION_LIST **i = modules; *i; i++) { + r = module_process( + friendly_name, + *i, + search_uri, + encrypted_key, + encrypted_key_size, + until, + ret_decrypted_key, + ret_decrypted_key_size); + if (r != -EAGAIN) + return r; + } + + return -EAGAIN; +} diff --git a/src/cryptsetup/cryptsetup-pkcs11.h b/src/cryptsetup/cryptsetup-pkcs11.h new file mode 100644 index 0000000000000..f207b6c749645 --- /dev/null +++ b/src/cryptsetup/cryptsetup-pkcs11.h @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +#include "time-util.h" + +#if HAVE_P11KIT + +int acquire_pkcs11_key( + const char *friendly_name, + const char *pkcs11_uri, + const char *key_file, + size_t key_file_size, + uint64_t key_file_offset, + usec_t until, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size); + +#else + +static inline int acquire_pkcs11_key( + const char *friendly_name, + const char *pkcs11_uri, + const char *key_file, + size_t key_file_size, + uint64_t key_file_offset, + usec_t until, + void **ret_decrypted_key, + size_t *ret_decrypted_key_size) { + + return -EOPNOTSUPP; +} + +#endif diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index 78732a0a577f9..f00fdb71e91b6 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -13,6 +13,7 @@ #include "alloc-util.h" #include "ask-password-api.h" #include "crypt-util.h" +#include "cryptsetup-pkcs11.h" #include "device-util.h" #include "escape.h" #include "fileio.h" @@ -58,11 +59,13 @@ static char **arg_tcrypt_keyfiles = NULL; static uint64_t arg_offset = 0; static uint64_t arg_skip = 0; static usec_t arg_timeout = USEC_INFINITY; +static char *arg_pkcs11_uri = NULL; STATIC_DESTRUCTOR_REGISTER(arg_cipher, freep); STATIC_DESTRUCTOR_REGISTER(arg_hash, freep); STATIC_DESTRUCTOR_REGISTER(arg_header, freep); STATIC_DESTRUCTOR_REGISTER(arg_tcrypt_keyfiles, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_pkcs11_uri, freep); /* Options Debian's crypttab knows we don't: @@ -251,7 +254,16 @@ static int parse_one_option(const char *option) { if (r < 0) return log_error_errno(r, "Failed to parse %s: %m", option); - } else if (!streq(option, "none")) + } else if ((val = startswith(option, "pkcs11-uri="))) { + + if (!startswith(val, "pkcs11:")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "pkcs11-uri= parameter expects a PKCS#11 URI, refusing"); + + r = free_and_strdup(&arg_pkcs11_uri, val); + if (r < 0) + return log_oom(); + + } else log_warning("Encountered unknown /etc/crypttab option '%s', ignoring.", option); return 0; @@ -276,10 +288,10 @@ static int parse_options(const char *options) { } /* sanity-check options */ - if (arg_type != NULL && !streq(arg_type, CRYPT_PLAIN)) { - if (arg_offset) + if (arg_type && !streq(arg_type, CRYPT_PLAIN)) { + if (arg_offset != 0) log_warning("offset= ignored with type %s", arg_type); - if (arg_skip) + if (arg_skip != 0) log_warning("skip= ignored with type %s", arg_type); } @@ -337,28 +349,19 @@ static char *disk_mount_point(const char *label) { return NULL; } -static int get_password(const char *vol, const char *src, usec_t until, bool accept_cached, char ***ret) { - _cleanup_free_ char *description = NULL, *name_buffer = NULL, *mount_point = NULL, *text = NULL, *disk_path = NULL; - _cleanup_strv_free_erase_ char **passwords = NULL; - const char *name = NULL; - char **p, *id; - int r = 0; +static char *friendly_disk_name(const char *src, const char *vol) { + _cleanup_free_ char *description = NULL, *mount_point = NULL; + char *name_buffer = NULL; + int r; - assert(vol); assert(src); - assert(ret); + assert(vol); description = disk_description(src); mount_point = disk_mount_point(vol); - disk_path = cescape(src); - if (!disk_path) - return log_oom(); - + /* If the description string is simply the volume name, then let's not show this twice */ if (description && streq(vol, description)) - /* If the description string is simply the - * volume name, then let's not show this - * twice */ description = mfree(description); if (mount_point && description) @@ -367,13 +370,39 @@ static int get_password(const char *vol, const char *src, usec_t until, bool acc r = asprintf(&name_buffer, "%s on %s", vol, mount_point); else if (description) r = asprintf(&name_buffer, "%s (%s)", description, vol); - + else + return strdup(vol); if (r < 0) + return NULL; + + return name_buffer; +} + +static int get_password( + const char *vol, + const char *src, + usec_t until, + bool accept_cached, + char ***ret) { + + _cleanup_free_ char *friendly = NULL, *text = NULL, *disk_path = NULL; + _cleanup_strv_free_erase_ char **passwords = NULL; + char **p, *id; + int r = 0; + + assert(vol); + assert(src); + assert(ret); + + friendly = friendly_disk_name(src, vol); + if (!friendly) return log_oom(); - name = name_buffer ? name_buffer : vol; + if (asprintf(&text, "Please enter passphrase for disk %s:", friendly) < 0) + return log_oom(); - if (asprintf(&text, "Please enter passphrase for disk %s:", name) < 0) + disk_path = cescape(src); + if (!disk_path) return log_oom(); id = strjoina("cryptsetup:", disk_path); @@ -389,7 +418,7 @@ static int get_password(const char *vol, const char *src, usec_t until, bool acc assert(strv_length(passwords) == 1); - if (asprintf(&text, "Please enter passphrase for disk %s (verification):", name) < 0) + if (asprintf(&text, "Please enter passphrase for disk %s (verification):", friendly) < 0) return log_oom(); id = strjoina("cryptsetup-verification:", disk_path); @@ -447,6 +476,11 @@ static int attach_tcrypt( assert(name); assert(key_file || (passwords && passwords[0])); + if (arg_pkcs11_uri) { + log_error("Sorry, but tcrypt devices are currently not supported in conjunction with pkcs11 support."); + return -EAGAIN; /* Ask for a regular password */ + } + if (arg_tcrypt_hidden) params.flags |= CRYPT_TCRYPT_HIDDEN_HEADER; @@ -487,17 +521,19 @@ static int attach_tcrypt( return 0; } -static int attach_luks_or_plain(struct crypt_device *cd, - const char *name, - const char *key_file, - char **passwords, - uint32_t flags) { +static int attach_luks_or_plain( + struct crypt_device *cd, + const char *name, + const char *key_file, + char **passwords, + uint32_t flags, + usec_t until) { + int r = 0; bool pass_volume_key = false; assert(cd); assert(name); - assert(key_file || passwords); if ((!arg_type && !crypt_get_type(cd)) || streq_ptr(arg_type, CRYPT_PLAIN)) { struct crypt_params_plain params = { @@ -553,7 +589,39 @@ static int attach_luks_or_plain(struct crypt_device *cd, crypt_get_volume_key_size(cd)*8, crypt_get_device_name(cd)); - if (key_file) { + if (arg_pkcs11_uri) { + _cleanup_free_ void *decrypted_key = NULL, *friendly = NULL; + size_t decrypted_key_size = 0; + + if (!key_file) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "PKCS#11 mode selected but no key file specified, refusing."); + + friendly = friendly_disk_name(crypt_get_device_name(cd), name); + if (!friendly) + return log_oom(); + + r = acquire_pkcs11_key( + friendly, + arg_pkcs11_uri, + key_file, + arg_keyfile_size, arg_keyfile_offset, + until, + &decrypted_key, &decrypted_key_size); + if (r < 0) + return r; + + if (pass_volume_key) + r = crypt_activate_by_volume_key(cd, name, decrypted_key, decrypted_key_size, flags); + else + r = crypt_activate_by_passphrase(cd, name, arg_key_slot, decrypted_key, decrypted_key_size, flags); + if (r == -EPERM) { + log_error_errno(r, "Failed to activate with PKCS#11 decrypted key. (Key incorrect?)"); + return -EAGAIN; /* log actual error, but return EAGAIN */ + } + if (r < 0) + return log_error_errno(r, "Failed to activate with PKCS#11 acquired key: %m"); + + } else if (key_file) { r = crypt_activate_by_keyfile_offset(cd, name, arg_key_slot, key_file, arg_keyfile_size, arg_keyfile_offset, flags); if (r == -EPERM) { log_error_errno(r, "Failed to activate with key file '%s'. (Key data incorrect?)", key_file); @@ -565,6 +633,7 @@ static int attach_luks_or_plain(struct crypt_device *cd, } if (r < 0) return log_error_errno(r, "Failed to activate with key file '%s': %m", key_file); + } else { char **p; @@ -659,25 +728,21 @@ static int run(int argc, char *argv[]) { if (argc < 4) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "attach requires at least two arguments."); - if (argc >= 5 && - argv[4][0] && - !streq(argv[4], "-") && - !streq(argv[4], "none")) { - - if (!path_is_absolute(argv[4])) - log_warning("Password file path '%s' is not absolute. Ignoring.", argv[4]); - else + if (argc >= 5 && !STR_IN_SET(argv[4], "", "-", "none")) { + if (path_is_absolute(argv[4])) key_file = argv[4]; + else + log_warning("Password file path '%s' is not absolute. Ignoring.", argv[4]); } - if (argc >= 6 && argv[5][0] && !streq(argv[5], "-")) { + if (argc >= 6 && !STR_IN_SET(argv[5], "", "-", "none")) { r = parse_options(argv[5]); if (r < 0) return r; } /* A delicious drop of snake oil */ - mlockall(MCL_FUTURE); + (void) mlockall(MCL_FUTURE); if (arg_header) { log_debug("LUKS header: %s", arg_header); @@ -740,7 +805,7 @@ static int run(int argc, char *argv[]) { for (tries = 0; arg_tries == 0 || tries < arg_tries; tries++) { _cleanup_strv_free_erase_ char **passwords = NULL; - if (!key_file) { + if (!key_file && !arg_pkcs11_uri) { r = get_password(argv[2], argv[3], until, tries == 0 && !arg_verify, &passwords); if (r == -EAGAIN) continue; @@ -751,11 +816,7 @@ static int run(int argc, char *argv[]) { if (streq_ptr(arg_type, CRYPT_TCRYPT)) r = attach_tcrypt(cd, argv[2], key_file, passwords, flags); else - r = attach_luks_or_plain(cd, - argv[2], - key_file, - passwords, - flags); + r = attach_luks_or_plain(cd, argv[2], key_file, passwords, flags, until); if (r >= 0) break; if (r != -EAGAIN) @@ -763,6 +824,7 @@ static int run(int argc, char *argv[]) { /* Passphrase not correct? Let's try again! */ key_file = NULL; + arg_pkcs11_uri = NULL; } if (arg_tries != 0 && tries >= arg_tries) diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 50de0afce6f00..c1be6c034c561 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -1,8 +1,9 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ #include -#include #include +#include +#include #include "architecture.h" #include "dissect-image.h" @@ -171,7 +172,7 @@ static int run(int argc, char *argv[]) { if (r <= 0) return r; - r = loop_device_make_by_path(arg_image, (arg_flags & DISSECT_IMAGE_READ_ONLY) ? O_RDONLY : O_RDWR, &d); + r = loop_device_make_by_path(arg_image, (arg_flags & DISSECT_IMAGE_READ_ONLY) ? O_RDONLY : O_RDWR, LO_FLAGS_PARTSCAN, &d); if (r < 0) return log_error_errno(r, "Failed to set up loopback device: %m"); diff --git a/src/fuzz/fuzz-json.c b/src/fuzz/fuzz-json.c index ce7b69dbb99f6..c01e2a570c50a 100644 --- a/src/fuzz/fuzz-json.c +++ b/src/fuzz/fuzz-json.c @@ -18,7 +18,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { f = fmemopen_unlocked((char*) data, size, "re"); assert_se(f); - if (json_parse_file(f, NULL, &v, NULL, NULL) < 0) + if (json_parse_file(f, NULL, 0, &v, NULL, NULL) < 0) return 0; g = open_memstream_unlocked(&out, &out_size); diff --git a/src/home/home-util.c b/src/home/home-util.c new file mode 100644 index 0000000000000..37f614425bc05 --- /dev/null +++ b/src/home/home-util.c @@ -0,0 +1,157 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#if HAVE_CRYPT_H +#include +#endif + +#include "dns-domain.h" +#include "errno-util.h" +#include "home-util.h" +#include "memory-util.h" +#include "path-util.h" +#include "string-util.h" +#include "strv.h" +#include "user-util.h" + +bool suitable_user_name(const char *name) { + + /* Checks whether the specified name is suitable for management via homed. Note that our client side + * usually validate susing a simple valid_user_group_name(), while server side we are a bit more + * restrictive, so that we can change the rules server side without having to update things client + * side, too. */ + + if (!valid_user_group_name(name)) + return false; + + /* We generally rely on NSS to tell us which users not to care for, but let's filter out some + * particularly well-known users. */ + if (STR_IN_SET(name, + "root", + "nobody", + NOBODY_USER_NAME, NOBODY_GROUP_NAME)) + return false; + + /* Let's also defend our own namespace, as well as Debian's (unwritten?) logic of prefixing system + * users with underscores. */ + if (STARTSWITH_SET(name, "systemd-", "_")) + return false; + + return true; +} + +int suitable_realm(const char *realm) { + _cleanup_free_ char *normalized = NULL; + int r; + + /* Similar to the above: let's validate the realm a bit stricter server-side than client side */ + + r = dns_name_normalize(realm, 0, &normalized); /* this also checks general validity */ + if (r == -EINVAL) + return 0; + if (r < 0) + return r; + + if (!streq(realm, normalized)) /* is this normalized? */ + return false; + + if (dns_name_is_root(realm) || dns_name_is_single_label(realm)) /* Don't allow top level domain nor single label domains */ + return false; + + return true; +} + +int suitable_image_path(const char *path) { + + return !empty_or_root(path) && + path_is_valid(path) && + path_is_absolute(path); +} + +int split_user_name_realm(const char *t, char **ret_user_name, char **ret_realm) { + _cleanup_free_ char *un = NULL, *rr = NULL; + const char *c; + int r; + + assert(t); + assert(ret_user_name); + assert(ret_realm); + + c = strchr(t, '@'); + if (!c) { + if (!suitable_user_name(t)) + return -EINVAL; + + un = strdup(t); + if (!un) + return -ENOMEM; + } else { + un = strndup(t, c - t); + if (!un) + return -ENOMEM; + + if (!suitable_user_name(un)) + return -EINVAL; + + r = suitable_realm(c + 1); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + + rr = strdup(c + 1); + if (!rr) + return -ENOMEM; + } + + *ret_user_name = TAKE_PTR(un); + *ret_realm = TAKE_PTR(rr); + + return 0; +} + +int bus_message_append_secret(sd_bus_message *m, UserRecord *secret) { + _cleanup_(erase_and_freep) char *formatted = NULL; + JsonVariant *v; + int r; + + assert(m); + assert(secret); + + if (!FLAGS_SET(secret->mask, USER_RECORD_SECRET)) + return -EINVAL; + + v = json_variant_by_key(secret->json, "secret"); + if (!v) + return -EINVAL; + + r = json_variant_format(v, 0, &formatted); + if (r < 0) + return r; + + return sd_bus_message_append(m, "s", formatted); +} + +int test_password(char **hashed_password, const char *password) { + char **hpw; + + STRV_FOREACH(hpw, hashed_password) { + struct crypt_data cc = {}; + const char *k; + bool b; + + errno = 0; + k = crypt_r(password, *hpw, &cc); + if (!k) { + explicit_bzero_safe(&cc, sizeof(cc)); + return errno_or_else(EINVAL); + } + + b = streq(k, *hpw); + explicit_bzero_safe(&cc, sizeof(cc)); + + if (b) + return true; + } + + return false; +} diff --git a/src/home/home-util.h b/src/home/home-util.h new file mode 100644 index 0000000000000..fea46d5e2e6a6 --- /dev/null +++ b/src/home/home-util.h @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +#include "sd-bus.h" + +#include "time-util.h" +#include "user-record.h" + +bool suitable_user_name(const char *name); +int suitable_realm(const char *realm); +int suitable_image_path(const char *path); + +int split_user_name_realm(const char *t, char **ret_user_name, char **ret_realm); + +int bus_message_append_secret(sd_bus_message *m, UserRecord *secret); + +/* Many of our operations might be slow due to crypto, fsck, recursive chown() and so on. For these + * operations permit a *very* long time-out */ +#define HOME_SLOW_BUS_CALL_TIMEOUT_USEC (2*USEC_PER_MINUTE) + +int test_password(char **hashed_password, const char *password); diff --git a/src/home/homectl.c b/src/home/homectl.c new file mode 100644 index 0000000000000..263eae3087272 --- /dev/null +++ b/src/home/homectl.c @@ -0,0 +1,2998 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "sd-bus.h" + +#include "alloc-util.h" +#include "ask-password-api.h" +#include "bus-common-errors.h" +#include "bus-error.h" +#include "bus-util.h" +#include "cgroup-util.h" +#include "dns-domain.h" +#include "env-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-table.h" +#include "format-util.h" +#include "fs-util.h" +#include "home-util.h" +#include "locale-util.h" +#include "main-func.h" +#include "memory-util.h" +#include "pager.h" +#include "parse-util.h" +#include "path-util.h" +#include "pretty-print.h" +#include "process-util.h" +#include "pwquality-util.h" +#include "rlimit-util.h" +#include "spawn-polkit-agent.h" +#include "terminal-util.h" +#include "user-record-show.h" +#include "user-record-util.h" +#include "user-record.h" +#include "user-util.h" +#include "verbs.h" + +static PagerFlags arg_pager_flags = 0; +static bool arg_legend = true; +static bool arg_ask_password = true; +static BusTransport arg_transport = BUS_TRANSPORT_LOCAL; +static const char *arg_host = NULL; +static const char *arg_identity = NULL; +static JsonVariant *arg_identity_extra = NULL; +static JsonVariant *arg_identity_extra_privileged = NULL; +static JsonVariant *arg_identity_extra_this_machine = NULL; +static JsonVariant *arg_identity_extra_rlimits = NULL; +static char **arg_identity_filter = NULL; /* this one is also applied to 'privileged' and 'thisMachine' subobjects */ +static char **arg_identity_filter_rlimits = NULL; +static uint64_t arg_disk_size = UINT64_MAX; +static uint64_t arg_disk_size_relative = UINT64_MAX; +static bool arg_json = false; +static JsonFormatFlags arg_json_format_flags = 0; +static enum { + EXPORT_FORMAT_FULL, /* export the full record */ + EXPORT_FORMAT_STRIPPED, /* strip "state" + "binding", but leave signature in place */ + EXPORT_FORMAT_MINIMAL, /* also strip signature */ +} arg_export_format = EXPORT_FORMAT_FULL; + +STATIC_DESTRUCTOR_REGISTER(arg_identity_extra, json_variant_unrefp); +STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_this_machine, json_variant_unrefp); +STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_privileged, json_variant_unrefp); +STATIC_DESTRUCTOR_REGISTER(arg_identity_extra_rlimits, json_variant_unrefp); +STATIC_DESTRUCTOR_REGISTER(arg_identity_filter, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_identity_filter_rlimits, strv_freep); + +static int acquire_bus(sd_bus **bus) { + int r; + + assert(bus); + + if (*bus) + return 0; + + r = bus_connect_transport(arg_transport, arg_host, false, bus); + if (r < 0) + return log_error_errno(r, "Failed to connect to bus: %m"); + + (void) sd_bus_set_allow_interactive_authorization(*bus, arg_ask_password); + + return 0; +} + +static int list_homes(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(table_unrefp) Table *table = NULL; + int r; + + (void) pager_open(arg_pager_flags); + + r = acquire_bus(&bus); + if (r < 0) + return r; + + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ListHomes", + &error, + &reply, + NULL); + if (r < 0) + return log_error_errno(r, "Failed to list homes: %s", bus_error_message(&error, r)); + + table = table_new("name", "uid", "gid", "state", "realname", "home", "shell"); + if (!table) + return log_oom(); + + r = sd_bus_message_enter_container(reply, 'a', "(susussso)"); + if (r < 0) + return bus_log_parse_error(r); + + for (;;) { + const char *name, *state, *realname, *home, *shell, *color; + TableCell *cell; + uint32_t uid, gid; + + r = sd_bus_message_read(reply, "(susussso)", &name, &uid, &state, &gid, &realname, &home, &shell, NULL); + if (r < 0) + return bus_log_parse_error(r); + if (r == 0) + break; + + r = table_add_many(table, + TABLE_STRING, name, + TABLE_UID, uid, + TABLE_GID, gid); + if (r < 0) + return log_error_errno(r, "Failed to add row to table: %m"); + + + r = table_add_cell(table, &cell, TABLE_STRING, state); + if (r < 0) + return log_error_errno(r, "Failed to add field to table: %m"); + + color = user_record_state_color(state); + if (color) + (void) table_set_color(table, cell, color); + + r = table_add_many(table, + TABLE_STRING, strna(empty_to_null(realname)), + TABLE_STRING, home, + TABLE_STRING, strna(empty_to_null(shell))); + if (r < 0) + return log_error_errno(r, "Failed to add row to table: %m"); + } + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + if (table_get_rows(table) > 1 || arg_json) { + r = table_set_sort(table, (size_t) 0, (size_t) -1); + if (r < 0) + return log_error_errno(r, "Failed to sort table: %m"); + + table_set_header(table, arg_legend); + + if (arg_json) + r = table_print_json(table, stdout, arg_json_format_flags); + else + r = table_print(table, NULL); + if (r < 0) + return log_error_errno(r, "Failed to show table: %m"); + } + + if (arg_legend && !arg_json) { + if (table_get_rows(table) > 1) + printf("\n%zu homes listed.\n", table_get_rows(table) - 1); + else + printf("No homes.\n"); + } + + return 0; +} + +static int acquire_existing_password(const char *user_name, UserRecord *hr) { + _cleanup_(strv_free_erasep) char **password = NULL; + _cleanup_free_ char *question = NULL; + char *e; + int r; + + assert(user_name); + assert(hr); + + e = getenv("PASSWORD"); + if (e) { + /* People really shouldn't use environment variables for passing passwords. We support this + * only for testing purposes, and do not document the behaviour, so that people won't + * actually use this outside of testing. */ + + r = user_record_set_password(hr, STRV_MAKE(e), true); + if (r < 0) + return log_error_errno(r, "Failed to store password: %m"); + + string_erase(e); + + if (unsetenv("PASSWORD") < 0) + return log_error_errno(errno, "Failed to unset $PASSWORD: %m"); + + return 0; + } + + if (asprintf(&question, "Please enter password for user %s:", user_name) < 0) + return log_oom(); + + r = ask_password_tty(-1, question, NULL, USEC_INFINITY, 0, NULL, &password); + if (r < 0) + return log_error_errno(r, "Failed to acquire password: %m"); + + r = user_record_set_password(hr, password, true); + if (r < 0) + return log_error_errno(r, "Failed to store password: %m"); + + return 0; +} + +static int activate_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r, ret = EXIT_SUCCESS; + char **i; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + + secret = user_record_new(); + if (!secret) + return log_oom(); + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = acquire_existing_password(*i, secret); + if (r < 0) + return r; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ActivateHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, secret); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) + log_error("Password incorrect, please try again."); + else { + log_error_errno(r, "Failed to activate user home: %s", bus_error_message(&error, r)); + if (ret == EXIT_SUCCESS) + ret = r; + + break; + } + } else + break; + } + } + + return ret; +} + +static int deactivate_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r, ret = EXIT_SUCCESS; + char **i; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "DeactivateHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + log_error_errno(r, "Failed to deactivate user home: %s", bus_error_message(&error, r)); + if (ret == EXIT_SUCCESS) + ret = r; + } + } + + return ret; +} + +static void dump_home_record(UserRecord *hr) { + int r; + + assert(hr); + + if (hr->incomplete) { + fflush(stdout); + log_warning("Warning: lacking rights to acquire privileged fields of user record of '%s', output incomplete.", hr->user_name); + } + + if (arg_json) { + _cleanup_(user_record_unrefp) UserRecord *stripped = NULL; + + if (arg_export_format == EXPORT_FORMAT_STRIPPED) + r = user_record_clone(hr, USER_RECORD_EXTRACT_EMBEDDED, &stripped); + else if (arg_export_format == EXPORT_FORMAT_MINIMAL) + r = user_record_clone(hr, USER_RECORD_EXTRACT_SIGNABLE, &stripped); + else + r = 0; + if (r < 0) + log_warning_errno(r, "Failed to strip user record, ignoring: %m"); + if (stripped) + hr = stripped; + + json_variant_dump(hr->json, arg_json_format_flags, stdout, NULL); + } else + user_record_show(hr, true); +} + +static char **mangle_user_list(char **list, char ***ret_allocated) { + _cleanup_free_ char *myself = NULL; + char **l; + + if (!strv_isempty(list)) { + *ret_allocated = NULL; + return list; + } + + myself = getusername_malloc(); + if (!myself) + return NULL; + + l = new(char*, 2); + if (!l) + return NULL; + + l[0] = TAKE_PTR(myself); + l[1] = NULL; + + *ret_allocated = l; + return l; +} + +static int inspect_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(strv_freep) char **mangled_list = NULL; + int r, ret = EXIT_SUCCESS; + char **items, **i; + + (void) pager_open(arg_pager_flags); + + r = acquire_bus(&bus); + if (r < 0) + return r; + + items = mangle_user_list(strv_skip(argv, 1), &mangled_list); + if (!items) + return log_oom(); + + STRV_FOREACH(i, items) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + const char *json; + int incomplete; + uid_t uid; + + r = parse_uid(*i, &uid); + if (r < 0) { + if (!valid_user_group_name(*i)) { + log_error("Invalid user name '%s'.", *i); + if (ret == EXIT_SUCCESS) + ret = -EINVAL; + + continue; + } + + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "GetUserRecordByName", + &error, + &reply, + "s", + *i); + } else { + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "GetUserRecordByUID", + &error, + &reply, + "u", + (uint32_t) uid); + } + + if (r < 0) { + log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r)); + if (ret == EXIT_SUCCESS) + ret = r; + + continue; + } + + r = sd_bus_message_read(reply, "sbo", &json, &incomplete, NULL); + if (r < 0) { + bus_log_parse_error(r); + if (ret == EXIT_SUCCESS) + ret = r; + + continue; + } + + r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL); + if (r < 0) { + log_error_errno(r, "Failed to parse JSON identity: %m"); + if (ret == EXIT_SUCCESS) + ret = r; + + continue; + } + + hr = user_record_new(); + if (!hr) + return log_oom(); + + r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_LOG); + if (r < 0) { + if (ret == EXIT_SUCCESS) + ret = r; + + continue; + } + + hr->incomplete = incomplete; + dump_home_record(hr); + } + + return ret; +} + +static int ssh_authorized_keys(int argc, char *argv[], void *userdata) { + _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_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + const char *json; + int r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + if (!valid_user_group_name(argv[1])) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid user name '%s'.", argv[1]); + + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "GetUserRecordByName", + &error, + &reply, + "s", + argv[1]); + if (r < 0) { + if (sd_bus_error_has_name(&error, SD_BUS_ERROR_SERVICE_UNKNOWN) || + sd_bus_error_has_name(&error, SD_BUS_ERROR_NAME_HAS_NO_OWNER)) { + log_debug_errno(r, "systemd-homed is not available: %s", bus_error_message(&error, r)); + return EXIT_SUCCESS; + } + + if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_HOME)) { + log_debug_errno(r, "Not a user managed by systemd-homed: %s", bus_error_message(&error, r)); + return EXIT_SUCCESS; + } + + return log_error_errno(r, "Failed to query user record for %s: %m", argv[1]); + } + + r = sd_bus_message_read(reply, "sbo", &json, NULL, NULL); + if (r < 0) + return bus_log_parse_error(r); + + r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse JSON record: %m"); + + hr = user_record_new(); + if (!hr) + return log_oom(); + + r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_LOG); + if (r < 0) + return r; + + if (strv_isempty(hr->ssh_authorized_keys)) + log_debug("User record for %s has no public SSH keys.", argv[1]); + else { + char **i; + + STRV_FOREACH(i, hr->ssh_authorized_keys) + printf("%s\n", *i); + } + + return EXIT_SUCCESS; +} + +static int authenticate_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(strv_freep) char **mangled_list = NULL; + int r, ret = EXIT_SUCCESS; + char **i, **items; + + items = mangle_user_list(strv_skip(argv, 1), &mangled_list); + if (!items) + return log_oom(); + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + STRV_FOREACH(i, items) { + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + + secret = user_record_new(); + if (!secret) + return log_oom(); + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = acquire_existing_password(*i, secret); + if (r < 0) + return r; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "AuthenticateHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, secret); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) + log_error("Password incorrect, please try again."); + else { + log_error_errno(r, "Failed to authenticate user home: %s", bus_error_message(&error, r)); + if (ret == EXIT_SUCCESS) + ret = r; + + break; + } + } else + break; + } + } + + return ret; +} + +static int update_last_change(JsonVariant **v, bool override) { + JsonVariant *c; + usec_t n; + int r; + + assert(v); + + n = now(CLOCK_REALTIME); + + c = json_variant_by_key(*v, "lastChangeUSec"); + if (c) { + uintmax_t u; + + if (!override) + return 0; + + if (!json_variant_is_unsigned(c)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "lastChangeUSec field is not an unsigned integer, refusing."); + + u = json_variant_unsigned(c); + if (u >= n) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "lastChangeUSec is from the future, can't update."); + } + + r = json_variant_set_field_unsigned(v, "lastChangeUSec", n); + if (r < 0) + return log_error_errno(r, "Failed to merge identities: %m"); + + return 1; +} + +static int apply_identity_changes(JsonVariant **_v) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + int r; + + assert(_v); + + v = json_variant_ref(*_v); + + r = json_variant_filter(&v, arg_identity_filter); + if (r < 0) + return log_error_errno(r, "Failed to filter identity: %m"); + + r = json_variant_merge(&v, arg_identity_extra); + if (r < 0) + return log_error_errno(r, "Failed to merge identities: %m"); + + if (arg_identity_extra_this_machine || !strv_isempty(arg_identity_filter)) { + _cleanup_(json_variant_unrefp) JsonVariant *per_machine = NULL, *mmid = NULL; + char mids[SD_ID128_STRING_MAX]; + sd_id128_t mid; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return log_error_errno(r, "Failed to acquire machine ID: %m"); + + r = json_variant_new_string(&mmid, sd_id128_to_string(mid, mids)); + if (r < 0) + return log_error_errno(r, "Failed to allocate matchMachineId object: %m"); + + per_machine = json_variant_ref(json_variant_by_key(v, "perMachine")); + if (per_machine) { + _cleanup_(json_variant_unrefp) JsonVariant *npm = NULL, *add = NULL; + _cleanup_free_ JsonVariant **array = NULL; + JsonVariant *z; + size_t i = 0; + + if (!json_variant_is_array(per_machine)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "perMachine field is not an array, refusing."); + + array = new(JsonVariant*, json_variant_elements(per_machine) + 1); + if (!array) + return log_oom(); + + JSON_VARIANT_ARRAY_FOREACH(z, per_machine) { + JsonVariant *u; + + if (!json_variant_is_object(z)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "perMachine entry is not an object, refusing."); + + array[i++] = z; + + u = json_variant_by_key(z, "matchMachineId"); + if (!u) + continue; + + if (!json_variant_equal(u, mmid)) + continue; + + r = json_variant_merge(&add, z); + if (r < 0) + return log_error_errno(r, "Failed to merge perMachine entry: %m"); + + i--; + } + + r = json_variant_filter(&add, arg_identity_filter); + if (r < 0) + return log_error_errno(r, "Failed to filter perMachine: %m"); + + r = json_variant_merge(&add, arg_identity_extra_this_machine); + if (r < 0) + return log_error_errno(r, "Failed to merge in perMachine fields: %m"); + + if (arg_identity_filter_rlimits || arg_identity_extra_rlimits) { + _cleanup_(json_variant_unrefp) JsonVariant *rlv = NULL; + + rlv = json_variant_ref(json_variant_by_key(add, "resourceLimits")); + + r = json_variant_filter(&rlv, arg_identity_filter_rlimits); + if (r < 0) + return log_error_errno(r, "Failed to filter resource limits: %m"); + + r = json_variant_merge(&rlv, arg_identity_extra_rlimits); + if (r < 0) + return log_error_errno(r, "Failed to set resource limits: %m"); + + if (json_variant_is_blank_object(rlv)) { + r = json_variant_filter(&add, STRV_MAKE("resourceLimits")); + if (r < 0) + return log_error_errno(r, "Failed to drop resource limits field from identity: %m"); + } else { + r = json_variant_set_field(&add, "resourceLimits", rlv); + if (r < 0) + return log_error_errno(r, "Failed to update resource limits of identity: %m"); + } + } + + if (!json_variant_is_blank_object(add)) { + r = json_variant_set_field(&add, "matchMachineId", mmid); + if (r < 0) + return log_error_errno(r, "Failed to set matchMachineId field: %m"); + + array[i++] = add; + } + + r = json_variant_new_array(&npm, array, i); + if (r < 0) + return log_error_errno(r, "Failed to allocate new perMachine array: %m"); + + json_variant_unref(per_machine); + per_machine = TAKE_PTR(npm); + } else { + _cleanup_(json_variant_unrefp) JsonVariant *item = json_variant_ref(arg_identity_extra_this_machine); + + if (arg_identity_extra_rlimits) { + r = json_variant_set_field(&item, "resourceLimits", arg_identity_extra_rlimits); + if (r < 0) + return log_error_errno(r, "Failed to update resource limits of identity: %m"); + } + + r = json_variant_set_field(&item, "matchMachineId", mmid); + if (r < 0) + return log_error_errno(r, "Failed to set matchMachineId field: %m"); + + r = json_variant_append_array(&per_machine, item); + if (r < 0) + return log_error_errno(r, "Failed to append to perMachine array: %m"); + } + + r = json_variant_set_field(&v, "perMachine", per_machine); + if (r < 0) + return log_error_errno(r, "Failed to update per machine record: %m"); + } + + if (arg_identity_extra_privileged || arg_identity_filter) { + _cleanup_(json_variant_unrefp) JsonVariant *privileged = NULL; + + privileged = json_variant_ref(json_variant_by_key(v, "privileged")); + + r = json_variant_filter(&privileged, arg_identity_filter); + if (r < 0) + return log_error_errno(r, "Failed to filter identity (privileged part): %m"); + + r = json_variant_merge(&privileged, arg_identity_extra_privileged); + if (r < 0) + return log_error_errno(r, "Failed to merge identities (privileged part): %m"); + + if (json_variant_is_blank_object(privileged)) { + r = json_variant_filter(&v, STRV_MAKE("privileged")); + if (r < 0) + return log_error_errno(r, "Failed to drop privileged part from identity: %m"); + } else { + r = json_variant_set_field(&v, "privileged", privileged); + if (r < 0) + return log_error_errno(r, "Failed to update privileged part of identity: %m"); + } + } + + if (arg_identity_filter_rlimits) { + _cleanup_(json_variant_unrefp) JsonVariant *rlv = NULL; + + rlv = json_variant_ref(json_variant_by_key(v, "resourceLimits")); + + r = json_variant_filter(&rlv, arg_identity_filter_rlimits); + if (r < 0) + return log_error_errno(r, "Failed to filter resource limits: %m"); + + /* Note that we only filter resource limits here, but don't apply them. We do that in the perMachine section */ + + if (json_variant_is_blank_object(rlv)) { + r = json_variant_filter(&v, STRV_MAKE("resourceLimits")); + if (r < 0) + return log_error_errno(r, "Failed to drop resource limits field from identity: %m"); + } else { + r = json_variant_set_field(&v, "resourceLimits", rlv); + if (r < 0) + return log_error_errno(r, "Failed to update resource limits of identity: %m"); + } + } + + if (DEBUG_LOGGING) + json_variant_dump(v, JSON_FORMAT_PRETTY, NULL, NULL); + + json_variant_unref(*_v); + *_v = TAKE_PTR(v); + + return 0; +} + +static int add_disposition(JsonVariant **v) { + int r; + + assert(v); + + if (json_variant_by_key(*v, "disposition")) + return 0; + + /* Set the disposition to regular, if not configured explicitly */ + r = json_variant_set_field_string(v, "disposition", "regular"); + if (r < 0) + return log_error_errno(r, "Failed to set disposition field: %m"); + + return 1; +} + +static int acquire_home_record(UserRecord **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + int r; + + assert(ret); + + hr = user_record_new(); + if (!hr) + return log_oom(); + + if (arg_identity) { + unsigned line, column; + + r = json_parse_file( + streq(arg_identity, "-") ? stdin : NULL, + streq(arg_identity, "-") ? "" : arg_identity, JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column); + } + + r = apply_identity_changes(&v); + if (r < 0) + return r; + + r = update_last_change(&v, false); + if (r < 0) + return r; + + r = add_disposition(&v); + if (r < 0) + return r; + + r = user_record_load(hr, v, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE|USER_RECORD_LOG); + if (r < 0) + return r; + + *ret = TAKE_PTR(hr); + return 0; +} + +static int acquire_new_password( + const char *user_name, + UserRecord *hr, + bool suggest) { + + unsigned i = 5; + char *e; + int r; + + assert(user_name); + assert(hr); + + e = getenv("NEWPASSWORD"); + if (e) { + /* As above, this is not for use, just for testing */ + + r = user_record_set_password(hr, STRV_MAKE(e), false); + if (r < 0) + return log_error_errno(r, "Failed to store password: %m"); + + string_erase(e); + + if (unsetenv("NEWPASSWORD") < 0) + return log_error_errno(errno, "Failed to unse $NEWPASSWORD: %m"); + + return 0; + } + + if (suggest) + (void) suggest_passwords(); + + for (;;) { + _cleanup_(strv_free_erasep) char **first = NULL, **second = NULL; + _cleanup_free_ char *question = NULL; + + if (--i == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Too many attempts, giving up:"); + + if (asprintf(&question, "Please enter new password for user %s:", user_name) < 0) + return log_oom(); + + r = ask_password_tty(-1, question, NULL, USEC_INFINITY, 0, NULL, &first); + if (r < 0) + return log_error_errno(r, "Failed to acquire password: %m"); + + question = mfree(question); + if (asprintf(&question, "Please enter new password for user %s (repeat):", user_name) < 0) + return log_oom(); + + r = ask_password_tty(-1, question, NULL, USEC_INFINITY, 0, NULL, &second); + if (r < 0) + return log_error_errno(r, "Failed to acquire password: %m"); + + if (strv_equal(first, second)) { + r = user_record_set_password(hr, first, false); + if (r < 0) + return log_error_errno(r, "Failed to store password: %m"); + + return 0; + } + + log_error("Password didn't mach, try again."); + } +} + +static int create_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + int r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + if (argc >= 2) { + /* If a username was specified, use it */ + + if (valid_user_group_name(argv[1])) + r = json_variant_set_field_string(&arg_identity_extra, "userName", argv[1]); + else { + _cleanup_free_ char *un = NULL, *rr = NULL; + + /* Before we consider the user name invalid, let's check if we can split it? */ + r = split_user_name_realm(argv[1], &un, &rr); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name '%s' is not valid: %m", argv[1]); + + if (rr) { + r = json_variant_set_field_string(&arg_identity_extra, "realm", rr); + if (r < 0) + return log_error_errno(r, "Failed to set realm field: %m"); + } + + r = json_variant_set_field_string(&arg_identity_extra, "userName", un); + } + if (r < 0) + return log_error_errno(r, "Failed to set userName field: %m"); + } else { + /* If neither a username nor an identity have been specified we cannot operate. */ + if (!arg_identity) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name required."); + } + + r = acquire_home_record(&hr); + if (r < 0) + return r; + + /* If the JSON record carries no secrets, then let's query them manually */ + if (!FLAGS_SET(hr->mask, USER_RECORD_SECRET)) { + + if (strv_isempty(hr->hashed_password)) { + /* No hashed passwords set in the record, let's fix that. */ + r = acquire_new_password(hr->user_name, hr, /* suggest = */ true); + if (r < 0) + return r; + + r = user_record_make_hashed_password(hr, hr); + if (r < 0) + return log_error_errno(r, "Failed to hash password: %m"); + } else { + /* There's a hash password set in the record, acquire the unhashed version of it. */ + r = acquire_existing_password(hr->user_name, hr); + if (r < 0) + return r; + + r = user_record_test_secret(hr, hr); + if (r < 0) + return log_error_errno(r, "Password does not match record."); + } + } + + if (hr->enforce_password_policy == 0) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + + /* If password quality enforcement is disabled, let's at least warn client side */ + + r = quality_check_password(hr, hr, &error); + if (r < 0) + log_warning_errno(r, "Specified password does not pass quality checks (%s), proceeding anyway.", bus_error_message(&error, r)); + } + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_(erase_and_freep) char *formatted = NULL; + + r = json_variant_format(hr->json, 0, &formatted); + if (r < 0) + return r; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "CreateHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", formatted); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (!sd_bus_error_has_name(&error, BUS_ERROR_LOW_PASSWORD_QUALITY)) + return log_error_errno(r, "Failed to create user home: %s", bus_error_message(&error, r)); + + log_error_errno(r, "%s", bus_error_message(&error, r)); + log_info("(Use --enforce-password-policy=no to turn off password quality checks for this account.)"); + } else + break; /* done */ + + r = acquire_new_password(hr->user_name, hr, /* suggest = */ false); + if (r < 0) + return r; + + r = user_record_make_hashed_password(hr, hr); + if (r < 0) + return log_error_errno(r, "Failed to hash passwords: %m"); + } + + return EXIT_SUCCESS; +} + +static int remove_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r, ret = EXIT_SUCCESS; + char **i; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "RemoveHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + log_error_errno(r, "Failed to remove home: %s", bus_error_message(&error, r)); + if (ret == EXIT_SUCCESS) + ret = r; + } + } + + return ret; +} + +static int update_home(int argc, char *argv[], void *userdata) { + _cleanup_(json_variant_unrefp) JsonVariant *json = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + _cleanup_free_ char *buffer = NULL; + const char *username; + int r; + + if (argc >= 2) + username = argv[1]; + else if (!arg_identity) { + buffer = getusername_malloc(); + if (!buffer) + return log_oom(); + + username = buffer; + } else + username = NULL; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + if (arg_identity) { + unsigned line, column; + JsonVariant *un; + + r = json_parse_file( + streq(arg_identity, "-") ? stdin : NULL, + streq(arg_identity, "-") ? "" : arg_identity, JSON_PARSE_SENSITIVE, &json, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column); + + un = json_variant_by_key(json, "userName"); + if (un) { + if (!json_variant_is_string(un) || (username && !streq(json_variant_string(un), username))) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User name specified on command line and in JSON record do not match."); + + if (!username) { + buffer = strdup(json_variant_string(un)); + if (!buffer) + return log_oom(); + + username = buffer; + } + } else { + if (!username) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No username specified."); + + r = json_variant_set_field_string(&arg_identity_extra, "userName", username); + if (r < 0) + return log_error_errno(r, "Failed to set userName field: %m"); + } + + } else { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + int incomplete; + const char *text; + + if (!arg_identity_extra && + !arg_identity_extra_this_machine && + !arg_identity_extra_privileged && + !arg_identity_extra_rlimits && + strv_isempty(arg_identity_filter) && + strv_isempty(arg_identity_filter_rlimits)) + return log_error_errno(SYNTHETIC_ERRNO(EALREADY), "No field to change specified."); + + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "GetUserRecordByName", + &error, + &reply, + "s", + username); + if (r < 0) + return log_error_errno(r, "Failed to acquire user home record: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "sbo", &text, &incomplete, NULL); + if (r < 0) + return bus_log_parse_error(r); + + if (incomplete) + return log_error_errno(SYNTHETIC_ERRNO(EACCES), "Lacking rights to acquire user record including privileged metadata, can't update record."); + + r = json_parse(text, JSON_PARSE_SENSITIVE, &json, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse JSON identity: %m"); + + reply = sd_bus_message_unref(reply); + + r = json_variant_filter(&json, STRV_MAKE("binding", "status", "signature")); + if (r < 0) + return log_error_errno(r, "Failed to strip binding and status from record to update: %m"); + } + + r = apply_identity_changes(&json); + if (r < 0) + return r; + + /* If the user supplied a full record, then add in lastChange, but do not override. Otherwise always override. */ + r = update_last_change(&json, !arg_identity); + if (r < 0) + return r; + + hr = user_record_new(); + if (!hr) + return log_oom(); + + r = user_record_load(hr, json, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SECRET|USER_RECORD_ALLOW_SIGNATURE|USER_RECORD_LOG); + if (r < 0) + return r; + + if (!FLAGS_SET(hr->mask, USER_RECORD_SECRET)) { + r = acquire_existing_password(username, hr); + if (r < 0) + return r; + } + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_free_ char *formatted = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "UpdateHome"); + if (r < 0) + return bus_log_create_error(r); + + r = json_variant_format(hr->json, 0, &formatted); + if (r < 0) + return r; + + r = sd_bus_message_append(m, "s", formatted); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (!sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) + return log_error_errno(r, "Failed to update home: %s", bus_error_message(&error, r)); + } else + break; + + log_error("Password incorrect, please try again."); + + r = acquire_existing_password(username, hr); + if (r < 0) + return r; + } + + return EXIT_SUCCESS; +} + +static int passwd_home(int argc, char *argv[], void *userdata) { + _cleanup_(user_record_unrefp) UserRecord *old_secret = NULL, *new_secret = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_free_ char *buffer = NULL; + const char *username; + int r; + + if (argc >= 2) + username = argv[1]; + else { + buffer = getusername_malloc(); + if (!buffer) + return log_oom(); + + username = buffer; + } + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + old_secret = user_record_new(); + if (!old_secret) + return log_oom(); + + r = acquire_existing_password(username, old_secret); + if (r < 0) + return r; + + new_secret = user_record_new(); + if (!new_secret) + return log_oom(); + + r = acquire_new_password(username, new_secret, /* suggest = */ true); + if (r < 0) + return r; + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ChangePasswordHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", username); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, new_secret); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, old_secret); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_LOW_PASSWORD_QUALITY)) { + + log_error_errno(r, "%s", bus_error_message(&error, r)); + + r = acquire_new_password(username, new_secret, /* suggest = */ false); + if (r < 0) + return r; + + continue; + + } else if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) { + + log_notice("Old password incorrect, please try again."); + + r = acquire_existing_password(username, old_secret); + if (r < 0) + return r; + + continue; + } else + return log_error_errno(r, "Failed to change password for home: %s", bus_error_message(&error, r)); + } else + break; + } + + return EXIT_SUCCESS; +} + +static int resize_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + uint64_t ds = UINT64_MAX; + int r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password); + + if (arg_disk_size_relative != UINT64_MAX || + (argc > 2 && parse_percent(argv[2]) >= 0)) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Relative disk size specification currently not supported when resizing."); + + if (argc > 2) { + r = parse_size(argv[2], 1024, &ds); + if (r < 0) + return log_error_errno(r, "Failed to parse disk size parameter: %s", argv[2]); + } + + if (arg_disk_size != UINT64_MAX) { + if (ds != UINT64_MAX && ds != arg_disk_size) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Disk size specified twice and doesn't match, refusing."); + + ds = arg_disk_size; + } + + secret = user_record_new(); + if (!secret) + return log_oom(); + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = acquire_existing_password(argv[1], secret); + if (r < 0) + return r; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ResizeHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "st", argv[1], ds); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, secret); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (!sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) + return log_error_errno(r, "Failed to resize home: %s", bus_error_message(&error, r)); + } else + break; + + log_error("Password incorrect, please try again."); + } + + return EXIT_SUCCESS; +} + +static int lock_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r, ret = EXIT_SUCCESS; + char **i; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "LockHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + log_error_errno(r, "Failed to lock home: %s", bus_error_message(&error, r)); + if (ret == EXIT_SUCCESS) + ret = r; + } + } + + return ret; +} + +static int unlock_home(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r, ret = EXIT_SUCCESS; + char **i; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + STRV_FOREACH(i, strv_skip(argv, 1)) { + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + + secret = user_record_new(); + if (!secret) + return log_oom(); + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = acquire_existing_password(*i, secret); + if (r < 0) + return r; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "UnlockHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", *i); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, secret); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) + log_error("Password incorrect, please try again."); + else { + log_error_errno(r, "Failed to unlock user home: %s", bus_error_message(&error, r)); + if (ret == EXIT_SUCCESS) + ret = r; + + break; + } + } else + break; + } + } + + return ret; +} + +static int with_home(int argc, char *argv[], void *userdata) { + _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; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + _cleanup_close_ int acquired_fd = -1; + _cleanup_strv_free_ char **cmdline = NULL; + const char *home; + int r, ret; + pid_t pid; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + if (argc < 3) { + _cleanup_free_ char *shell = NULL; + + /* If no command is specified, spawn a shell */ + r = get_shell(&shell); + if (r < 0) + return log_error_errno(r, "Failed to acquire shell: %m"); + + cmdline = strv_new(shell); + } else + cmdline = strv_copy(argv + 2); + if (!cmdline) + return log_oom(); + + secret = user_record_new(); + if (!secret) + return log_oom(); + + for (;;) { + r = acquire_existing_password(argv[1], secret); + if (r < 0) + return r; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "AcquireHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", argv[1]); + if (r < 0) + return bus_log_create_error(r); + + r = bus_message_append_secret(m, secret); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "b", /* please_suspend = */ getenv_bool("SYSTEMD_PLEASE_SUSPEND_HOME") > 0); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply); + m = sd_bus_message_unref(m); + if (r < 0) { + if (!sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) + return log_error_errno(r, "Failed to activate user home: %s", bus_error_message(&error, r)); + + log_error("Password incorrect, please try again."); + sd_bus_error_free(&error); + } else { + int fd; + + r = sd_bus_message_read(reply, "h", &fd); + if (r < 0) + return bus_log_parse_error(r); + + acquired_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3); + if (acquired_fd < 0) + return log_error_errno(errno, "Failed to duplicate acquired fd: %m"); + + reply = sd_bus_message_unref(reply); + break; + } + } + + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "GetHomeByName", + &error, + &reply, + "s", + argv[1]); + if (r < 0) + return log_error_errno(r, "Failed to inspect home: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "usussso", NULL, NULL, NULL, NULL, &home, NULL, NULL); + if (r < 0) + return bus_log_parse_error(r); + + r = safe_fork("(with)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_LOG|FORK_RLIMIT_NOFILE_SAFE|FORK_REOPEN_LOG, &pid); + if (r < 0) + return r; + if (r == 0) { + if (chdir(home) < 0) { + log_error_errno(errno, "Failed to change to directory %s: %m", home); + _exit(255); + } + + execvp(cmdline[0], cmdline); + log_error_errno(errno, "Failed to execute %s: %m", cmdline[0]); + _exit(255); + } + + ret = wait_for_terminate_and_check(cmdline[0], pid, WAIT_LOG_ABNORMAL); + + /* Close the fd that pings the home now. */ + acquired_fd = safe_close(acquired_fd); + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ReleaseHome"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", argv[1]); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_BUSY)) + log_notice("Not deactivating home directory of %s, as it is still used.", argv[1]); + else + return log_error_errno(r, "Failed to release user home: %s", bus_error_message(&error, r)); + } + + return ret; +} + +static int lock_all_homes(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r; + + r = acquire_bus(&bus); + if (r < 0) + return r; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "LockAllHomes"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) + return log_error_errno(r, "Failed to lock home: %s", bus_error_message(&error, r)); + + return EXIT_SUCCESS; +} + +static int drop_from_identity(const char *field) { + int r; + + assert(field); + + /* If we are called to update an identity record and drop some field, let's keep track of what to + * remove from the old record */ + r = strv_extend(&arg_identity_filter, field); + if (r < 0) + return log_oom(); + + /* Let's also drop the field if it was previously set to a new value on the same command line */ + r = json_variant_filter(&arg_identity_extra, STRV_MAKE(field)); + if (r < 0) + return log_error_errno(r, "Failed to filter JSON identity data: %m"); + + r = json_variant_filter(&arg_identity_extra_this_machine, STRV_MAKE(field)); + if (r < 0) + return log_error_errno(r, "Failed to filter JSON identity data: %m"); + + return 0; +} + +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("homectl", "1", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] {COMMAND} ...\n\n" + "Create, manipulate or inspect home directories.\n\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" + " --no-ask-password Do not ask for system passwords\n" + " -H --host=[USER@]HOST Operate on remote host\n" + " -M --machine=CONTAINER Operate on local container\n" + " --identity=PATH Read JSON identity from file\n" + " --json=FORMAT Output inspection data in JSON (takes one of\n" + " pretty, short, off)\n" + " -j Equivalent to --json=pretty (on TTY) or\n" + " --json=short (otherwise)\n" + " --export-format= Strip JSON inspection data (full, stripped,\n" + " minimal)\n" + " -E When specified once equals -j --export-format=\n" + " stripped, when specified twice equals\n" + " -j --export-format=minimal\n" + "\n%3$sGeneral User Record Properties:%4$s\n" + " -c --real-name=REALNAME Real name for user\n" + " --realm=REALM Realm to create user in\n" + " --email-address=EMAIL Email address for user\n" + " --location=LOCATION Set location of user on earth\n" + " --password-hint=HINT Set Password hint\n" + " --icon-name=NAME Icon name for user\n" + " -d --home-dir=PATH Home directory\n" + " --uid=UID Numeric UID for user\n" + " -G --member-of=GROUP Add user to group\n" + " --skel=PATH Skeleton directory to use\n" + " --shell=PATH Shell for account\n" + " --setenv=VARIABLE=VALUE Set an environment variable at log-in\n" + " --timezone=TIMEZONE Set a time-zone\n" + " --language=LOCALE Set preferred language\n" + " --ssh-authorized-keys=KEYS\n" + " Specify SSH public keys\n" + "\n%3$sAccount Management User Record Properties:%4$s\n" + " --locked=BOOL Set locked account state\n" + " --not-before=TIMESTAMP Do not allow logins before\n" + " --not-after=TIMESTAMP Do not allow logins after\n" + " --rate-limit-interval=SECS\n" + " Login rate-limit interval in seconds\n" + " --rate-limit-burst=NUMBER\n" + " Login rate-limit attempts per interval\n" + " --enforce-password-policy=BOOL\n" + " Control whether to enforce system's password\n" + " policy for this user\n" + " -P Equivalent to --enforce-password-password=no\n" + "\n%3$sResource Management User Record Properties:%4$s\n" + " --disk-size=BYTES Size to assign the user on disk\n" + " --access-mode=MODE User home directory access mode\n" + " --umask=MODE Umask for user when logging in\n" + " --nice=NICE Nice level for user\n" + " --rlimit=LIMIT=VALUE[:VALUE]\n" + " Set resource limits\n" + " --tasks-max=MAX Set maximum number of per-user tasks\n" + " --memory-high=BYTES Set high memory threshold in bytes\n" + " --memory-max=BYTES Set maximum memory limit\n" + " --cpu-weight=WEIGHT Set CPU weight\n" + " --io-weight=WEIGHT Set IO weight\n" + "\n%3$sStorage User Record Properties:%4$s\n" + " --storage=STORAGE Storage type to use (luks, fscrypt, directory,\n" + " subvolume, cifs)\n" + " --image-path=PATH Path to image file/directory\n" + "\n%3$sLUKS Storage User Record Properties:%4$s\n" + " --fs-type=TYPE File system type to use in case of luks\n" + " storage (ext4, xfs, btrfs)\n" + " --luks-discard=BOOL Whether to use 'discard' feature of file system\n" + " --luks-cipher=CIPHER Cipher to use for LUKS encryption\n" + " --luks-cipher-mode=MODE Cipher mode to use for LUKS encryption\n" + " --luks-volume-key-size=BITS\n" + " Volume key size to use for LUKS encryption\n" + " --luks-pbkdf-type=TYPE Password-based Key Derivation Function to use\n" + " --luks-pbkdf-hash-algorithm=ALGORITHM\n" + " PBKDF hash algorithm to use\n" + " --luks-pbkdf-time-cost=SECS\n" + " Time cost for PBKDF in seconds\n" + " --luks-pbkdf-memory-cost=BYTES\n" + " Memory cost for PBKDF in bytes\n" + " --luks-pbkdf-parallel-threads=NUMBER\n" + " Number of parallel threads for PKBDF\n" + "\n%3$sMounting User Record Properties:%4$s\n" + " --nosuid=BOOL Control the 'nosuid' flag of the home mount\n" + " --nodev=BOOL Control the 'nodev' flag of the home mount\n" + " --noexec=BOOL Control the 'noexec' flag of the home mount\n" + "\n%3$sCIFS User Record Properties:%4$s\n" + " --cifs-domain=DOMAIN CIFS (Windows) domain\n" + " --cifs-user-name=USER CIFS (Windows) user name\n" + " --cifs-service=SERVICE CIFS (Windows) service to mount as home\n" + "\n%3$sLogin Behaviour User Record Properties:%4$s\n" + " --stop-delay=SECS How long to leave user services running after\n" + " logout\n" + " --kill-processes=BOOL Whether to kill user processes when sessions\n" + " terminate\n" + " --auto-login=BOOL Try to log this user in automatically\n" + "\n%3$sCommands:%4$s\n" + " list List homes\n" + " activate USER… Activate home\n" + " deactivate USER… Deactivate home\n" + " inspect USER… Inspect home\n" + " authenticate USER… Authenticate home\n" + " create USER Create a home\n" + " remove USER… Remove a home\n" + " update USER Update a home\n" + " passwd USER Change password of a home\n" + " resize USER SIZE Resize a home\n" + " lock USER… Temporarily lock an active home\n" + " unlock USER… Unlock a temporarily locked home\n" + " lock-all Lock all suitable homes\n" + " with USER [COMMAND…] Run shell or command with access to home\n" + "\nSee the %2$s for details.\n" + , program_invocation_short_name + , link + , ansi_underline(), ansi_normal() + ); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + + enum { + ARG_VERSION = 0x100, + ARG_NO_PAGER, + ARG_NO_LEGEND, + ARG_NO_ASK_PASSWORD, + ARG_REALM, + ARG_EMAIL_ADDRESS, + ARG_DISK_SIZE, + ARG_ACCESS_MODE, + ARG_STORAGE, + ARG_FS_TYPE, + ARG_IMAGE_PATH, + ARG_UMASK, + ARG_LUKS_DISCARD, + ARG_JSON, + ARG_SETENV, + ARG_TIMEZONE, + ARG_LANGUAGE, + ARG_LOCKED, + ARG_SSH_AUTHORIZED_KEYS, + ARG_LOCATION, + ARG_ICON_NAME, + ARG_PASSWORD_HINT, + ARG_NICE, + ARG_RLIMIT, + ARG_NOT_BEFORE, + ARG_NOT_AFTER, + ARG_LUKS_CIPHER, + ARG_LUKS_CIPHER_MODE, + ARG_LUKS_VOLUME_KEY_SIZE, + ARG_NOSUID, + ARG_NODEV, + ARG_NOEXEC, + ARG_CIFS_DOMAIN, + ARG_CIFS_USER_NAME, + ARG_CIFS_SERVICE, + ARG_TASKS_MAX, + ARG_MEMORY_HIGH, + ARG_MEMORY_MAX, + ARG_CPU_WEIGHT, + ARG_IO_WEIGHT, + ARG_LUKS_PBKDF_TYPE, + ARG_LUKS_PBKDF_HASH_ALGORITHM, + ARG_LUKS_PBKDF_TIME_COST, + ARG_LUKS_PBKDF_MEMORY_COST, + ARG_LUKS_PBKDF_PARALLEL_THREADS, + ARG_RATE_LIMIT_INTERVAL, + ARG_RATE_LIMIT_BURST, + ARG_STOP_DELAY, + ARG_KILL_PROCESSES, + ARG_ENFORCE_PASSWORD_POLICY, + ARG_EXPORT_FORMAT, + ARG_AUTO_LOGIN, + }; + + 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 }, + { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD }, + { "host", required_argument, NULL, 'H' }, + { "machine", required_argument, NULL, 'M' }, + { "identity", required_argument, NULL, 'I' }, + { "real-name", required_argument, NULL, 'c' }, + { "comment", required_argument, NULL, 'c' }, /* Compat alias to keep thing in sync with useradd(8) */ + { "realm", required_argument, NULL, ARG_REALM }, + { "email-address", required_argument, NULL, ARG_EMAIL_ADDRESS }, + { "location", required_argument, NULL, ARG_LOCATION }, + { "password-hint", required_argument, NULL, ARG_PASSWORD_HINT }, + { "icon-name", required_argument, NULL, ARG_ICON_NAME }, + { "home-dir", required_argument, NULL, 'd' }, /* Compatible with useradd(8) */ + { "uid", required_argument, NULL, 'u' }, /* Compatible with useradd(8) */ + { "member-of", required_argument, NULL, 'G' }, + { "groups", required_argument, NULL, 'G' }, /* Compat alias to keep thing in sync with useradd(8) */ + { "skel", required_argument, NULL, 'k' }, /* Compatible with useradd(8) */ + { "shell", required_argument, NULL, 's' }, /* Compatible with useradd(8) */ + { "setenv", required_argument, NULL, ARG_SETENV }, + { "timezone", required_argument, NULL, ARG_TIMEZONE }, + { "language", required_argument, NULL, ARG_LANGUAGE }, + { "locked", required_argument, NULL, ARG_LOCKED }, + { "not-before", required_argument, NULL, ARG_NOT_BEFORE }, + { "not-after", required_argument, NULL, ARG_NOT_AFTER }, + { "expiredate", required_argument, NULL, 'e' }, /* Compat alias to keep thing in sync with useradd(8) */ + { "ssh-authorized-keys", required_argument, NULL, ARG_SSH_AUTHORIZED_KEYS }, + { "disk-size", required_argument, NULL, ARG_DISK_SIZE }, + { "access-mode", required_argument, NULL, ARG_ACCESS_MODE }, + { "umask", required_argument, NULL, ARG_UMASK }, + { "nice", required_argument, NULL, ARG_NICE }, + { "rlimit", required_argument, NULL, ARG_RLIMIT }, + { "tasks-max", required_argument, NULL, ARG_TASKS_MAX }, + { "memory-high", required_argument, NULL, ARG_MEMORY_HIGH }, + { "memory-max", required_argument, NULL, ARG_MEMORY_MAX }, + { "cpu-weight", required_argument, NULL, ARG_CPU_WEIGHT }, + { "io-weight", required_argument, NULL, ARG_IO_WEIGHT }, + { "storage", required_argument, NULL, ARG_STORAGE }, + { "image-path", required_argument, NULL, ARG_IMAGE_PATH }, + { "fs-type", required_argument, NULL, ARG_FS_TYPE }, + { "luks-discard", required_argument, NULL, ARG_LUKS_DISCARD }, + { "luks-cipher", required_argument, NULL, ARG_LUKS_CIPHER }, + { "luks-cipher-mode", required_argument, NULL, ARG_LUKS_CIPHER_MODE }, + { "luks-volume-key-size", required_argument, NULL, ARG_LUKS_VOLUME_KEY_SIZE }, + { "luks-pbkdf-type", required_argument, NULL, ARG_LUKS_PBKDF_TYPE }, + { "luks-pbkdf-hash-algorithm", required_argument, NULL, ARG_LUKS_PBKDF_HASH_ALGORITHM }, + { "luks-pbkdf-time-cost", required_argument, NULL, ARG_LUKS_PBKDF_TIME_COST }, + { "luks-pbkdf-memory-cost", required_argument, NULL, ARG_LUKS_PBKDF_MEMORY_COST }, + { "luks-pbkdf-parallel-threads", required_argument, NULL, ARG_LUKS_PBKDF_PARALLEL_THREADS }, + { "nosuid", required_argument, NULL, ARG_NOSUID }, + { "nodev", required_argument, NULL, ARG_NODEV }, + { "noexec", required_argument, NULL, ARG_NOEXEC }, + { "cifs-user-name", required_argument, NULL, ARG_CIFS_USER_NAME }, + { "cifs-domain", required_argument, NULL, ARG_CIFS_DOMAIN }, + { "cifs-service", required_argument, NULL, ARG_CIFS_SERVICE }, + { "rate-limit-interval", required_argument, NULL, ARG_RATE_LIMIT_INTERVAL }, + { "rate-limit-burst", required_argument, NULL, ARG_RATE_LIMIT_BURST }, + { "stop-delay", required_argument, NULL, ARG_STOP_DELAY }, + { "kill-processes", required_argument, NULL, ARG_KILL_PROCESSES }, + { "enforce-password-policy", required_argument, NULL, ARG_ENFORCE_PASSWORD_POLICY }, + { "auto-login", required_argument, NULL, ARG_AUTO_LOGIN }, + { "json", required_argument, NULL, ARG_JSON }, + { "export-format", required_argument, NULL, ARG_EXPORT_FORMAT }, + {} + }; + + int r; + + assert(argc >= 0); + assert(argv); + + for (;;) { + int c; + + c = getopt_long(argc, argv, "hH:M:I:c:d:u:k:s:e:G:jPE", 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_NO_ASK_PASSWORD: + arg_ask_password = false; + break; + + case 'H': + arg_transport = BUS_TRANSPORT_REMOTE; + arg_host = optarg; + break; + + case 'M': + arg_transport = BUS_TRANSPORT_MACHINE; + arg_host = optarg; + break; + + case 'I': + arg_identity = optarg; + break; + + case 'c': + if (isempty(optarg)) { + r = drop_from_identity("realName"); + if (r < 0) + return r; + + break; + } + + if (!valid_gecos(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Real name '%s' not a valid GECOS field.", optarg); + + r = json_variant_set_field_string(&arg_identity_extra, "realName", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set realName field: %m"); + + break; + + case 'd': { + _cleanup_free_ char *hd = NULL; + + if (isempty(optarg)) { + r = drop_from_identity("homeDirectory"); + if (r < 0) + return r; + + break; + } + + r = parse_path_argument_and_warn(optarg, false, &hd); + if (r < 0) + return r; + + if (!valid_home(hd)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Home directory '%s' not valid.", hd); + + r = json_variant_set_field_string(&arg_identity_extra, "homeDirectory", hd); + if (r < 0) + return log_error_errno(r, "Failed to set homeDirectory field: %m"); + + break; + } + + case ARG_REALM: + if (isempty(optarg)) { + r = drop_from_identity("realm"); + if (r < 0) + return r; + + break; + } + + r = dns_name_is_valid(optarg); + if (r < 0) + return log_error_errno(r, "Failed to determine whether realm '%s' is a valid DNS domain: %m", optarg); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Realm '%s' is not a valid DNS domain: %m", optarg); + + r = json_variant_set_field_string(&arg_identity_extra, "realm", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set realm field: %m"); + break; + + case ARG_EMAIL_ADDRESS: + case ARG_LOCATION: + case ARG_ICON_NAME: + case ARG_CIFS_USER_NAME: + case ARG_CIFS_DOMAIN: + case ARG_CIFS_SERVICE: { + + const char *field = + c == ARG_EMAIL_ADDRESS ? "emailAddress" : + c == ARG_LOCATION ? "location" : + c == ARG_ICON_NAME ? "iconName" : + c == ARG_CIFS_USER_NAME ? "cifsUserName" : + c == ARG_CIFS_DOMAIN ? "cifsDomain" : + c == ARG_CIFS_SERVICE ? "cifsService" : + NULL; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + break; + } + + r = json_variant_set_field_string(&arg_identity_extra, field, optarg); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case ARG_PASSWORD_HINT: + if (isempty(optarg)) { + r = drop_from_identity("passwordHint"); + if (r < 0) + return r; + + break; + } + + r = json_variant_set_field_string(&arg_identity_extra_privileged, "passwordHint", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set passwordHint field: %m"); + + string_erase(optarg); + break; + + case ARG_NICE: { + int nc; + + if (isempty(optarg)) { + r = drop_from_identity("niceLevel"); + if (r < 0) + return r; + break; + } + + r = parse_nice(optarg, &nc); + if (r < 0) + return log_error_errno(r, "Failed to parse nice level: %s", optarg); + + r = json_variant_set_field_integer(&arg_identity_extra, "niceLevel", nc); + if (r < 0) + return log_error_errno(r, "Failed to set niceLevel field: %m"); + + break; + } + + case ARG_RLIMIT: { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *jcur = NULL, *jmax = NULL; + _cleanup_free_ char *field = NULL, *t = NULL; + const char *eq; + struct rlimit rl; + int l; + + if (isempty(optarg)) { + /* Remove all resource limits */ + + r = drop_from_identity("resourceLimits"); + if (r < 0) + return r; + + arg_identity_filter_rlimits = strv_free(arg_identity_filter_rlimits); + arg_identity_extra_rlimits = json_variant_unref(arg_identity_extra_rlimits); + break; + } + + eq = strchr(optarg, '='); + if (!eq) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't parse resource limit assignment: %s", optarg); + + field = strndup(optarg, eq - optarg); + if (!field) + return log_oom(); + + l = rlimit_from_string_harder(field); + if (l < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown resource limit type: %s", field); + + if (isempty(eq + 1)) { + /* Remove only the specific rlimit */ + + r = strv_extend(&arg_identity_filter_rlimits, rlimit_to_string(l)); + if (r < 0) + return r; + + r = json_variant_filter(&arg_identity_extra_rlimits, STRV_MAKE(field)); + if (r < 0) + return log_error_errno(r, "Failed to filter JSON identity data: %m"); + + break; + } + + r = rlimit_parse(l, eq + 1, &rl); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse resource limit value: %s", eq + 1); + + r = rl.rlim_cur == RLIM_INFINITY ? json_variant_new_null(&jcur) : json_variant_new_unsigned(&jcur, rl.rlim_cur); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to allocate current integer: %m"); + + r = rl.rlim_max == RLIM_INFINITY ? json_variant_new_null(&jmax) : json_variant_new_unsigned(&jmax, rl.rlim_max); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to allocate maximum integer: %m"); + + r = json_build(&v, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("cur", JSON_BUILD_VARIANT(jcur)), + JSON_BUILD_PAIR("max", JSON_BUILD_VARIANT(jmax)))); + if (r < 0) + return log_error_errno(r, "Failed to build resource limit: %m"); + + t = strjoin("RLIMIT_", rlimit_to_string(l)); + if (!t) + return log_oom(); + + r = json_variant_set_field(&arg_identity_extra_rlimits, t, v); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", rlimit_to_string(l)); + + break; + } + + case 'u': { + uid_t uid; + + if (isempty(optarg)) { + r = drop_from_identity("uid"); + if (r < 0) + return r; + + break; + } + + r = parse_uid(optarg, &uid); + if (r < 0) + return log_error_errno(r, "Failed to parse UID '%s'.", optarg); + + if (uid_is_system(uid)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID " UID_FMT " is in system range, refusing.", uid); + if (uid_is_dynamic(uid)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID " UID_FMT " is in dynamic range, refusing.", uid); + if (uid == UID_NOBODY) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID " UID_FMT " is nobody UID, refusing.", uid); + + r = json_variant_set_field_unsigned(&arg_identity_extra, "uid", uid); + if (r < 0) + return log_error_errno(r, "Failed to set realm field: %m"); + + break; + } + + case 'k': + case ARG_IMAGE_PATH: { + const char *field = c == 'k' ? "skeletonDirectory" : "imagePath"; + _cleanup_free_ char *v = NULL; + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + break; + } + + r = parse_path_argument_and_warn(optarg, false, &v); + if (r < 0) + return r; + + r = json_variant_set_field_string(&arg_identity_extra_this_machine, field, v); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", v); + + break; + } + + case 's': + if (isempty(optarg)) { + r = drop_from_identity("shell"); + if (r < 0) + return r; + + break; + } + + if (!valid_shell(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Shell '%s' not valid.", optarg); + + r = json_variant_set_field_string(&arg_identity_extra, "shell", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set shell field: %m"); + + break; + + case ARG_SETENV: { + _cleanup_free_ char **l = NULL, **k = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *ne = NULL; + JsonVariant *e; + + if (isempty(optarg)) { + r = drop_from_identity("environment"); + if (r < 0) + return r; + + break; + } + + if (!env_assignment_is_valid(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Environment assignment '%s' not valid.", optarg); + + e = json_variant_by_key(arg_identity_extra, "environment"); + if (e) { + r = json_variant_strv(e, &l); + if (r < 0) + return log_error_errno(r, "Failed to parse JSON environment field: %m"); + } + + k = strv_env_set(l, optarg); + if (!k) + return log_oom(); + + strv_sort(k); + + r = json_variant_new_array_strv(&ne, k); + if (r < 0) + return log_error_errno(r, "Failed to allocate environment list JSON: %m"); + + r = json_variant_set_field(&arg_identity_extra, "environment", ne); + if (r < 0) + return log_error_errno(r, "Failed to set environent list: %m"); + + break; + } + + case ARG_TIMEZONE: + + if (isempty(optarg)) { + r = drop_from_identity("timeZone"); + if (r < 0) + return r; + + break; + } + + if (!timezone_is_valid(optarg, LOG_DEBUG)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Timezone '%s' is not valid.", optarg); + + r = json_variant_set_field_string(&arg_identity_extra, "timeZone", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set timezone field: %m"); + + break; + + case ARG_LANGUAGE: + if (isempty(optarg)) { + r = drop_from_identity("language"); + if (r < 0) + return r; + + break; + } + + if (!locale_is_valid(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale '%s' is not valid.", optarg); + + r = json_variant_set_field_string(&arg_identity_extra, "preferredLanguage", optarg); + if (r < 0) + return log_error_errno(r, "Failed to set preferredLanguage field: %m"); + + break; + + case ARG_NOSUID: + case ARG_NODEV: + case ARG_NOEXEC: + case ARG_LOCKED: + case ARG_KILL_PROCESSES: + case ARG_ENFORCE_PASSWORD_POLICY: + case ARG_AUTO_LOGIN: { + const char *field = + c == ARG_LOCKED ? "locked" : + c == ARG_NOSUID ? "mountNoSUID" : + c == ARG_NODEV ? "mountNoDevices" : + c == ARG_NOEXEC ? "mountNoExecute" : + c == ARG_KILL_PROCESSES ? "killProcesses" : + c == ARG_ENFORCE_PASSWORD_POLICY ? "enforcePasswordPolicy" : + c == ARG_AUTO_LOGIN ? "autoLogin" : + NULL; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + break; + } + + r = parse_boolean(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse %s boolean: %m", field); + + r = json_variant_set_field_boolean(&arg_identity_extra, field, r > 0); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case 'P': + r = json_variant_set_field_boolean(&arg_identity_extra, "enforcePasswordPolicy", false); + if (r < 0) + return log_error_errno(r, "Failed to set enforcePasswordPolicy field: %m"); + + break; + + case ARG_DISK_SIZE: + if (isempty(optarg)) { + r = drop_from_identity("diskSize"); + if (r < 0) + return r; + + r = drop_from_identity("diskSizeRelative"); + if (r < 0) + return r; + + arg_disk_size = arg_disk_size_relative = UINT64_MAX; + break; + } + + r = parse_permille(optarg); + if (r < 0) { + r = parse_size(optarg, 1024, &arg_disk_size); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Disk size '%s' not valid.", optarg); + + r = drop_from_identity("diskSizeRelative"); + if (r < 0) + return r; + + r = json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "diskSize", arg_disk_size); + if (r < 0) + return log_error_errno(r, "Failed to set diskSize field: %m"); + + arg_disk_size_relative = UINT64_MAX; + } else { + /* Normalize to UINT32_MAX == 100% */ + arg_disk_size_relative = (uint64_t) r * UINT32_MAX / 1000U; + + r = drop_from_identity("diskSize"); + if (r < 0) + return r; + + r = json_variant_set_field_unsigned(&arg_identity_extra_this_machine, "diskSizeRelative", arg_disk_size_relative); + if (r < 0) + return log_error_errno(r, "Failed to set diskSizeRelative field: %m"); + + arg_disk_size = UINT64_MAX; + } + + break; + + case ARG_ACCESS_MODE: { + mode_t mode; + + if (isempty(optarg)) { + r = drop_from_identity("accessMode"); + if (r < 0) + return r; + + break; + } + + r = parse_mode(optarg, &mode); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Access mode '%s' not valid.", optarg); + + r = json_variant_set_field_unsigned(&arg_identity_extra, "accessMode", mode); + if (r < 0) + return log_error_errno(r, "Failed to set access mode field: %m"); + + break; + } + + case ARG_LUKS_DISCARD: + if (isempty(optarg)) { + r = drop_from_identity("luksDiscard"); + if (r < 0) + return r; + + break; + } + + r = parse_boolean(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse --luks-discard= parameter: %s", optarg); + + r = json_variant_set_field_boolean(&arg_identity_extra, "luksDiscard", r); + if (r < 0) + return log_error_errno(r, "Failed to set discard field: %m"); + + break; + + case ARG_LUKS_VOLUME_KEY_SIZE: + case ARG_LUKS_PBKDF_PARALLEL_THREADS: + case ARG_RATE_LIMIT_BURST: { + const char *field = + c == ARG_LUKS_VOLUME_KEY_SIZE ? "luksVolumeKeySize" : + c == ARG_LUKS_PBKDF_PARALLEL_THREADS ? "luksPbkdfParallelThreads" : + c == ARG_RATE_LIMIT_BURST ? "rateLimitBurst" : NULL; + unsigned n; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + } + + r = safe_atou(optarg, &n); + if (r < 0) + return log_error_errno(r, "Failed to parse %s parameter: %s", field, optarg); + + r = json_variant_set_field_unsigned(&arg_identity_extra, field, n); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case ARG_UMASK: { + mode_t m; + + if (isempty(optarg)) { + r = drop_from_identity("umask"); + if (r < 0) + return r; + + break; + } + + r = parse_mode(optarg, &m); + if (r < 0) + return log_error_errno(r, "Failed to parse umask: %m"); + + r = json_variant_set_field_integer(&arg_identity_extra, "umask", m); + if (r < 0) + return log_error_errno(r, "Failed to set umask field: %m"); + + break; + } + + case ARG_SSH_AUTHORIZED_KEYS: { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *p = NULL; + _cleanup_(strv_freep) char **l = NULL, **add = NULL; + + if (isempty(optarg)) { + r = drop_from_identity("sshAuthorizedKeys"); + if (r < 0) + return r; + + break; + } + + if (optarg[0] == '@') { + _cleanup_fclose_ FILE *f = NULL; + + /* If prefixed with '@' read from a file */ + + f = fopen(optarg+1, "re"); + if (!f) + return log_error_errno(errno, "Failed to open '%s': %m", optarg+1); + + for (;;) { + _cleanup_free_ char *line = NULL; + + r = read_line(f, LONG_LINE_MAX, &line); + if (r < 0) + return log_error_errno(r, "Faile dto read from '%s': %m", optarg+1); + if (r == 0) + break; + + if (isempty(line)) + continue; + + if (line[0] == '#') + continue; + + r = strv_consume(&add, TAKE_PTR(line)); + if (r < 0) + return log_oom(); + } + } else { + /* Otherwise, assume it's a literal key. Let's do some superficial checks + * before accept it though. */ + + if (string_has_cc(optarg, NULL)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Authorized key contains control characters, refusing."); + if (optarg[0] == '#') + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified key is a comment?"); + + add = strv_new(optarg); + if (!add) + return log_oom(); + } + + v = json_variant_ref(json_variant_by_key(arg_identity_extra_privileged, "sshAuthorizedKeys")); + if (v) { + r = json_variant_strv(v, &l); + if (r < 0) + return log_error_errno(r, "Failed to parse SSH authorized keys list: %m"); + } + + r = strv_extend_strv(&l, add, true); + if (r < 0) + return log_oom(); + + v = json_variant_unref(v); + + r = json_variant_new_array_strv(&v, l); + if (r < 0) + return log_oom(); + + r = json_variant_set_field(&arg_identity_extra_privileged, "sshAuthorizedKeys", v); + if (r < 0) + return log_error_errno(r, "Failed to set authorized keys: %m"); + + break; + } + + case ARG_NOT_BEFORE: + case ARG_NOT_AFTER: + case 'e': { + const char *field; + usec_t n; + + field = c == ARG_NOT_BEFORE ? "notBeforeUSec" : + IN_SET(c, ARG_NOT_AFTER, 'e') ? "notAfterUSec" : NULL; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + break; + } + + /* Note the minor discrepancy regarding -e parsing here: we support that for compat + * reasons, and in the original useradd(8) implementation it accepts dates in the + * format YYYY-MM-DD. Coincidentally, we accept dates formatted like that too, but + * with greater precision. */ + r = parse_timestamp(optarg, &n); + if (r < 0) + return log_error_errno(r, "Failed to parse %s parameter: %m", field); + + r = json_variant_set_field_unsigned(&arg_identity_extra, field, n); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + break; + } + + case ARG_STORAGE: + case ARG_FS_TYPE: + case ARG_LUKS_CIPHER: + case ARG_LUKS_CIPHER_MODE: + case ARG_LUKS_PBKDF_TYPE: + case ARG_LUKS_PBKDF_HASH_ALGORITHM: { + + const char *field = + c == ARG_STORAGE ? "storage" : + c == ARG_FS_TYPE ? "fileSytemType" : + c == ARG_LUKS_CIPHER ? "luksCipher" : + c == ARG_LUKS_CIPHER_MODE ? "luksCipherMode" : + c == ARG_LUKS_PBKDF_TYPE ? "luksPbkdfType" : + c == ARG_LUKS_PBKDF_HASH_ALGORITHM ? "luksPbkdfHashAlgorithm" : NULL; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + break; + } + + if (!string_is_safe(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Parameter for %s field not valid: %s", field, optarg); + + r = json_variant_set_field_string( + IN_SET(c, ARG_STORAGE, ARG_FS_TYPE) ? + &arg_identity_extra_this_machine : + &arg_identity_extra, field, optarg); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case ARG_LUKS_PBKDF_TIME_COST: + case ARG_RATE_LIMIT_INTERVAL: + case ARG_STOP_DELAY: { + const char *field = + c == ARG_LUKS_PBKDF_TIME_COST ? "luksPbkdfTimeCostUSec" : + c == ARG_RATE_LIMIT_INTERVAL ? "rateLimitIntervalUSec" : + c == ARG_STOP_DELAY ? "stopDelayUSec" : + NULL; + usec_t t; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + + break; + } + + r = parse_sec(optarg, &t); + if (r < 0) + return log_error_errno(r, "Failed to parse %s field: %s", field, optarg); + + r = json_variant_set_field_unsigned(&arg_identity_extra, field, t); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case 'G': { + const char *p = optarg; + + if (isempty(p)) { + r = drop_from_identity("memberOf"); + if (r < 0) + return r; + + break; + } + + for (;;) { + _cleanup_(json_variant_unrefp) JsonVariant *mo = NULL; + _cleanup_strv_free_ char **list = NULL; + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&p, &word, ",", 0); + if (r < 0) + return log_error_errno(r, "Failed to parse group list: %m"); + if (r == 0) + break; + + if (!valid_user_group_name(word)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid group name %s.", word); + + mo = json_variant_ref(json_variant_by_key(arg_identity_extra, "memberOf")); + + r = json_variant_strv(mo, &list); + if (r < 0) + return log_error_errno(r, "Failed to parse group list: %m"); + + r = strv_extend(&list, word); + if (r < 0) + return log_error_errno(r, "Failed to extend group list: %m"); + + strv_sort(list); + strv_uniq(list); + + mo = json_variant_unref(mo); + r = json_variant_new_array_strv(&mo, list); + if (r < 0) + return log_error_errno(r, "Failed to create group list JSON: %m"); + + r = json_variant_set_field(&arg_identity_extra, "memberOf", mo); + if (r < 0) + return log_error_errno(r, "Failed to update group list: %m"); + } + + break; + } + + case ARG_TASKS_MAX: { + uint64_t u; + + if (isempty(optarg)) { + r = drop_from_identity("tasksMax"); + if (r < 0) + return r; + break; + } + + r = safe_atou64(optarg, &u); + if (r < 0) + return log_error_errno(r, "Failed to parse --tasks-max= parameter: %s", optarg); + + r = json_variant_set_field_unsigned(&arg_identity_extra, "tasksMax", u); + if (r < 0) + return log_error_errno(r, "Failed to set tasksMax field: %m"); + + break; + } + + case ARG_MEMORY_MAX: + case ARG_MEMORY_HIGH: + case ARG_LUKS_PBKDF_MEMORY_COST: { + const char *field = + c == ARG_MEMORY_MAX ? "memoryMax" : + c == ARG_MEMORY_HIGH ? "memoryHigh" : + c == ARG_LUKS_PBKDF_MEMORY_COST ? "luksPbkdfMemoryCost" : NULL; + + uint64_t u; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + break; + } + + r = parse_size(optarg, 1024, &u); + if (r < 0) + return log_error_errno(r, "Failed to parse %s parameter: %s", field, optarg); + + r = json_variant_set_field_unsigned(&arg_identity_extra_this_machine, field, u); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case ARG_CPU_WEIGHT: + case ARG_IO_WEIGHT: { + const char *field = c == ARG_CPU_WEIGHT ? "cpuWeight" : + c == ARG_IO_WEIGHT ? "ioWeight" : NULL; + uint64_t u; + + assert(field); + + if (isempty(optarg)) { + r = drop_from_identity(field); + if (r < 0) + return r; + break; + } + + r = safe_atou64(optarg, &u); + if (r < 0) + return log_error_errno(r, "Failed to parse --cpu-weight=/--io-weight= parameter: %s", optarg); + + if (!CGROUP_WEIGHT_IS_OK(u)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Weight %" PRIu64 " is out of valid weight range.", u); + + r = json_variant_set_field_unsigned(&arg_identity_extra, field, u); + if (r < 0) + return log_error_errno(r, "Failed to set %s field: %m", field); + + break; + } + + case 'j': + arg_json = true; + arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO; + break; + + case ARG_JSON: + if (streq(optarg, "pretty")) { + arg_json = true; + arg_json_format_flags = JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO; + } else if (streq(optarg, "short")) { + arg_json = true; + arg_json_format_flags = JSON_FORMAT_NEWLINE; + } else if (streq(optarg, "off")) { + arg_json = false; + arg_json_format_flags = 0; + } else if (streq(optarg, "help")) { + puts("pretty\n" + "short\n" + "off"); + return 0; + } else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown argument to --json=: %s", optarg); + + break; + + case 'E': + if (arg_export_format == EXPORT_FORMAT_FULL) + arg_export_format = EXPORT_FORMAT_STRIPPED; + else if (arg_export_format == EXPORT_FORMAT_STRIPPED) + arg_export_format = EXPORT_FORMAT_MINIMAL; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specifying -E more than twice is not supported."); + + arg_json = true; + if (arg_json_format_flags == 0) + arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO; + break; + + case ARG_EXPORT_FORMAT: + if (streq(optarg, "full")) + arg_export_format = EXPORT_FORMAT_FULL; + else if (streq(optarg, "stripped")) + arg_export_format = EXPORT_FORMAT_STRIPPED; + else if (streq(optarg, "minimal")) + arg_export_format = EXPORT_FORMAT_MINIMAL; + else if (streq(optarg, "help")) { + puts("full\n" + "stripped\n" + "minimal"); + return 0; + } + + 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 }, + { "list", VERB_ANY, 1, VERB_DEFAULT, list_homes }, + { "activate", 2, VERB_ANY, 0, activate_home }, + { "deactivate", 2, VERB_ANY, 0, deactivate_home }, + { "inspect", VERB_ANY, VERB_ANY, 0, inspect_home }, + { "authenticate", VERB_ANY, VERB_ANY, 0, authenticate_home }, + { "create", VERB_ANY, 2, 0, create_home }, + { "remove", 2, VERB_ANY, 0, remove_home }, + { "update", VERB_ANY, 2, 0, update_home }, + { "passwd", VERB_ANY, 2, 0, passwd_home }, + { "resize", 2, 3, 0, resize_home }, + { "lock", 2, VERB_ANY, 0, lock_home }, + { "unlock", 2, VERB_ANY, 0, unlock_home }, + { "with", 2, VERB_ANY, 0, with_home }, + { "lock-all", VERB_ANY, 1, 0, lock_all_homes }, + + /* 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; + + return dispatch_verb(argc, argv, verbs, NULL); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/home/homed-bus.c b/src/home/homed-bus.c new file mode 100644 index 0000000000000..0193089668d71 --- /dev/null +++ b/src/home/homed-bus.c @@ -0,0 +1,64 @@ +#include "homed-bus.h" +#include "strv.h" + +int bus_message_read_secret(sd_bus_message *m, UserRecord **ret, sd_bus_error *error) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *full = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + unsigned line = 0, column = 0; + const char *json; + int r; + + assert(ret); + + r = sd_bus_message_read(m, "s", &json); + if (r < 0) + return r; + + r = json_parse(json, JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Failed to parse JSON secret record at %u:%u: %m", line, column); + + r = json_build(&full, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("secret", JSON_BUILD_VARIANT(v)))); + if (r < 0) + return r; + + hr = user_record_new(); + if (!hr) + return -ENOMEM; + + r = user_record_load(hr, full, USER_RECORD_REQUIRE_SECRET); + if (r < 0) + return r; + + *ret = TAKE_PTR(hr); + return 0; +} + +int bus_message_read_home_record(sd_bus_message *m, UserRecordLoadFlags flags, UserRecord **ret, sd_bus_error *error) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + unsigned line = 0, column = 0; + const char *json; + int r; + + assert(ret); + + r = sd_bus_message_read(m, "s", &json); + if (r < 0) + return r; + + r = json_parse(json, JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Failed to parse JSON identity record at %u:%u: %m", line, column); + + hr = user_record_new(); + if (!hr) + return -ENOMEM; + + r = user_record_load(hr, v, flags); + if (r < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "JSON data is not a valid identity record"); + + *ret = TAKE_PTR(hr); + return 0; +} diff --git a/src/home/homed-bus.h b/src/home/homed-bus.h new file mode 100644 index 0000000000000..20f13b43ade38 --- /dev/null +++ b/src/home/homed-bus.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "sd-bus.h" + +#include "user-record.h" +#include "json.h" + +int bus_message_read_secret(sd_bus_message *m, UserRecord **ret, sd_bus_error *error); +int bus_message_read_home_record(sd_bus_message *m, UserRecordLoadFlags flags, UserRecord **ret, sd_bus_error *error); diff --git a/src/home/homed-home-bus.c b/src/home/homed-home-bus.c new file mode 100644 index 0000000000000..0f9229aafbd05 --- /dev/null +++ b/src/home/homed-home-bus.c @@ -0,0 +1,877 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "bus-common-errors.h" +#include "bus-util.h" +#include "fd-util.h" +#include "homed-bus.h" +#include "homed-home-bus.h" +#include "homed-home.h" +#include "strv.h" +#include "user-record-util.h" +#include "user-util.h" + +static int property_get_unix_record( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Home *h = userdata; + + assert(bus); + assert(reply); + assert(h); + + return sd_bus_message_append( + reply, "(suusss)", + h->user_name, + (uint32_t) h->uid, + h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID, + h->record ? user_record_real_name(h->record) : NULL, + h->record ? user_record_home_directory(h->record) : NULL, + h->record ? user_record_shell(h->record) : NULL); +} + +static int property_get_state( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Home *h = userdata; + + assert(bus); + assert(reply); + assert(h); + + return sd_bus_message_append(reply, "s", home_state_to_string(home_get_state(h))); +} + +int bus_home_client_is_trusted(Home *h, sd_bus_message *message) { + _cleanup_(sd_bus_creds_unrefp) sd_bus_creds *creds = NULL; + uid_t euid; + int r; + + assert(h); + + if (!message) + return -EINVAL; + + r = sd_bus_query_sender_creds(message, SD_BUS_CREDS_EUID, &creds); + if (r < 0) + return r; + + r = sd_bus_creds_get_euid(creds, &euid); + if (r < 0) + return r; + + return euid == 0 || h->uid == euid; +} + +int bus_home_get_record_json( + Home *h, + sd_bus_message *message, + char **ret, + bool *ret_incomplete) { + + _cleanup_(user_record_unrefp) UserRecord *augmented = NULL; + UserRecordLoadFlags flags; + int r, trusted; + + assert(h); + assert(ret); + + trusted = bus_home_client_is_trusted(h, message); + if (trusted < 0) { + log_warning_errno(trusted, "Failed to determine whether client is trusted, assuming untrusted."); + trusted = false; + } + + 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 = home_augment_status(h, flags, &augmented); + if (r < 0) + return r; + + r = json_variant_format(augmented->json, 0, ret); + if (r < 0) + return r; + + if (ret_incomplete) + *ret_incomplete = augmented->incomplete; + + return 0; +} + +static int property_get_user_record( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + _cleanup_free_ char *json = NULL; + Home *h = userdata; + bool incomplete; + int r; + + assert(bus); + assert(reply); + assert(h); + + r = bus_home_get_record_json(h, sd_bus_get_current_message(bus), &json, &incomplete); + if (r < 0) + return r; + + return sd_bus_message_append(reply, "(sb)", json, incomplete); +} + +int bus_home_method_activate( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = home_activate(h, secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + /* The operation is now in process, keep track of this message so that we can later reply to it. */ + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_deactivate( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = home_deactivate(h, false, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_unregister( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.remove-home", + NULL, + true, + UID_INVALID, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_unregister(h, error); + if (r < 0) + return r; + + assert(r > 0); + + /* Note that home_unregister() destroyed 'h' here, so no more accesses */ + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_home_method_realize( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.create-home", + NULL, + true, + UID_INVALID, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_create(h, secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + h->unregister_on_failure = false; + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_remove( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.remove-home", + NULL, + true, + UID_INVALID, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_remove(h, error); + if (r < 0) + return r; + if (r > 0) /* Done already. Note that home_remove() destroyed 'h' here, so no more accesses */ + return sd_bus_reply_method_return(message, NULL); + + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_fixate( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = home_fixate(h, secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_authenticate( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.authenticate-home", + NULL, + true, + h->uid, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_authenticate(h, secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_update_record(Home *h, sd_bus_message *message, UserRecord *hr, sd_bus_error *error) { + int r; + + assert(h); + assert(message); + assert(hr); + + r = user_record_is_supported(hr, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.update-home", + NULL, + true, + UID_INVALID, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_update(h, hr, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_update( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_home_record(message, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_REQUIRE_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE, &hr, error); + if (r < 0) + return r; + + return bus_home_method_update_record(h, message, hr, error); +} + +int bus_home_method_resize( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + Home *h = userdata; + uint64_t sz; + int r; + + assert(message); + assert(h); + + r = sd_bus_message_read(message, "t", &sz); + if (r < 0) + return r; + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.resize-home", + NULL, + true, + UID_INVALID, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_resize(h, sz, secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_change_password( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *new_secret = NULL, *old_secret = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &new_secret, error); + if (r < 0) + return r; + + r = bus_message_read_secret(message, &old_secret, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.passwd-home", + NULL, + true, + h->uid, + &h->manager->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = home_passwd(h, new_secret, old_secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_lock( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = home_lock(h, error); + if (r < 0) + return r; + if (r > 0) /* Done */ + return sd_bus_reply_method_return(message, NULL); + + /* The operation is now in process, keep track of this message so that we can later reply to it. */ + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_unlock( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = home_unlock(h, secret, error); + if (r < 0) + return r; + + assert(r == 0); + assert(!h->current_operation); + + /* The operation is now in process, keep track of this message so that we can later reply to it. */ + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_acquire( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + _cleanup_(operation_unrefp) Operation *o = NULL; + _cleanup_close_ int fd = -1; + int r, please_suspend; + Home *h = userdata; + + assert(message); + assert(h); + + r = bus_message_read_secret(message, &secret, error); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "b", &please_suspend); + if (r < 0) + return r; + + /* This operation might not be something we can executed immediately, hence queue it */ + fd = home_create_fifo(h, please_suspend); + if (fd < 0) + return sd_bus_reply_method_errnof(message, fd, "Failed to allocate fifo for %s: %m", h->user_name); + + o = operation_new(OPERATION_ACQUIRE, message); + if (!o) + return -ENOMEM; + + o->secret = TAKE_PTR(secret); + o->send_fd = TAKE_FD(fd); + + r = home_schedule_operation(h, o, error); + if (r < 0) + return r; + + return 1; +} + +int bus_home_method_ref( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_close_ int fd = -1; + Home *h = userdata; + HomeState state; + int please_suspend, r; + + assert(message); + assert(h); + + r = sd_bus_message_read(message, "b", &please_suspend); + if (r < 0) + return r; + + state = home_get_state(h); + switch (state) { + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_UNFIXATED: + case HOME_INACTIVE: + return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + default: + if (HOME_STATE_IS_ACTIVE(state)) + break; + + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + fd = home_create_fifo(h, please_suspend); + if (fd < 0) + return sd_bus_reply_method_errnof(message, fd, "Failed to allocate fifo for %s: %m", h->user_name); + + return sd_bus_reply_method_return(message, "h", fd); +} + +int bus_home_method_release( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(operation_unrefp) Operation *o = NULL; + Home *h = userdata; + int r; + + assert(message); + assert(h); + + o = operation_new(OPERATION_RELEASE, message); + if (!o) + return -ENOMEM; + + r = home_schedule_operation(h, o, error); + if (r < 0) + return r; + + return 1; +} + +/* We map a uid_t as uint32_t bus property, let's ensure this is safe. */ +assert_cc(sizeof(uid_t) == sizeof(uint32_t)); + +const sd_bus_vtable home_vtable[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_PROPERTY("UserName", "s", NULL, offsetof(Home, user_name), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("UID", "u", NULL, offsetof(Home, uid), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("UnixRecord", "(suusss)", property_get_unix_record, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("State", "s", property_get_state, 0, 0), + SD_BUS_PROPERTY("UserRecord", "(sb)", property_get_user_record, 0, SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Activate", "s", NULL, bus_home_method_activate, SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Deactivate", NULL, NULL, bus_home_method_deactivate, 0), + SD_BUS_METHOD("Unregister", NULL, NULL, bus_home_method_unregister, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("Realize", "s", NULL, bus_home_method_realize, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Remove", NULL, NULL, bus_home_method_remove, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("Fixate", "s", NULL, bus_home_method_fixate, SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Authenticate", "s", NULL, bus_home_method_authenticate, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Update", "s", NULL, bus_home_method_update, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Resize", "ts", NULL, bus_home_method_resize, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("ChangePassword", "ss", NULL, bus_home_method_change_password, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Lock", NULL, NULL, bus_home_method_lock, 0), + SD_BUS_METHOD("Unlock", "s", NULL, bus_home_method_unlock, SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Acquire", "sb", "h", bus_home_method_acquire, SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("Ref", "b", "h", bus_home_method_ref, 0), + SD_BUS_METHOD("Release", NULL, NULL, bus_home_method_release, 0), + SD_BUS_VTABLE_END +}; + +int bus_home_path(Home *h, char **ret) { + assert(ret); + + return sd_bus_path_encode("/org/freedesktop/home1/home", h->user_name, ret); +} + +int bus_home_object_find( + sd_bus *bus, + const char *path, + const char *interface, + void *userdata, + void **found, + sd_bus_error *error) { + + _cleanup_free_ char *e = NULL; + Manager *m = userdata; + uid_t uid; + Home *h; + int r; + + r = sd_bus_path_decode(path, "/org/freedesktop/home1/home", &e); + if (r <= 0) + return 0; + + if (parse_uid(e, &uid) >= 0) + h = hashmap_get(m->homes_by_uid, UID_TO_PTR(uid)); + else + h = hashmap_get(m->homes_by_name, e); + if (!h) + return 0; + + *found = h; + return 1; +} + +int bus_home_node_enumerator( + sd_bus *bus, + const char *path, + void *userdata, + char ***nodes, + sd_bus_error *error) { + + _cleanup_strv_free_ char **l = NULL; + Manager *m = userdata; + size_t k = 0; + Iterator i; + Home *h; + int r; + + assert(nodes); + + l = new0(char*, hashmap_size(m->homes_by_uid) + 1); + if (!l) + return -ENOMEM; + + HASHMAP_FOREACH(h, m->homes_by_uid, i) { + r = bus_home_path(h, l + k); + if (r < 0) + return r; + } + + *nodes = TAKE_PTR(l); + return 1; +} + +static int on_deferred_change(sd_event_source *s, void *userdata) { + _cleanup_free_ char *path = NULL; + Home *h = userdata; + int r; + + assert(h); + + h->deferred_change_event_source = sd_event_source_unref(h->deferred_change_event_source); + + r = bus_home_path(h, &path); + if (r < 0) { + log_warning_errno(r, "Failed to generate home bus path, ignoring: %m"); + return 0; + } + + if (h->announced) + r = sd_bus_emit_properties_changed_strv(h->manager->bus, path, "org.freedesktop.home1.Home", NULL); + else + r = sd_bus_emit_object_added(h->manager->bus, path); + if (r < 0) + log_warning_errno(r, "Failed to send home change event, ignoring: %m"); + else + h->announced = true; + + return 0; +} + +int bus_home_emit_change(Home *h) { + int r; + + assert(h); + + if (h->deferred_change_event_source) + return 1; + + if (!h->manager->event) + return 0; + + if (IN_SET(sd_event_get_state(h->manager->event), SD_EVENT_FINISHED, SD_EVENT_EXITING)) + return 0; + + r = sd_event_add_defer(h->manager->event, &h->deferred_change_event_source, on_deferred_change, h); + if (r < 0) + return log_error_errno(r, "Failed to allocate deferred change event source: %m"); + + r = sd_event_source_set_priority(h->deferred_change_event_source, SD_EVENT_PRIORITY_IDLE+5); + if (r < 0) + log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m"); + + (void) sd_event_source_set_description(h->deferred_change_event_source, "deferred-change-event"); + return 1; +} + +int bus_home_emit_remove(Home *h) { + _cleanup_free_ char *path = NULL; + int r; + + assert(h); + + if (!h->announced) + return 0; + + r = bus_home_path(h, &path); + if (r < 0) + return r; + + r = sd_bus_emit_object_removed(h->manager->bus, path); + if (r < 0) + return r; + + h->announced = false; + return 1; +} diff --git a/src/home/homed-home-bus.h b/src/home/homed-home-bus.h new file mode 100644 index 0000000000000..20516b1205350 --- /dev/null +++ b/src/home/homed-home-bus.h @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "sd-bus.h" + +#include "homed-home.h" + +int bus_home_client_is_trusted(Home *h, sd_bus_message *message); +int bus_home_get_record_json(Home *h, sd_bus_message *message, char **ret, bool *ret_incomplete); + +int bus_home_method_activate(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_deactivate(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_unregister(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_realize(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_remove(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_fixate(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_authenticate(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_update(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_update_record(Home *home, sd_bus_message *message, UserRecord *hr, sd_bus_error *error); +int bus_home_method_resize(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_change_password(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_lock(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_unlock(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_acquire(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_ref(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_home_method_release(sd_bus_message *message, void *userdata, sd_bus_error *error); + +extern const sd_bus_vtable home_vtable[]; + +int bus_home_path(Home *h, char **ret); + +int bus_home_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error); +int bus_home_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error); + +int bus_home_emit_change(Home *h); +int bus_home_emit_remove(Home *h); diff --git a/src/home/homed-home.c b/src/home/homed-home.c new file mode 100644 index 0000000000000..f553a40a4b873 --- /dev/null +++ b/src/home/homed-home.c @@ -0,0 +1,2684 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#if HAVE_LINUX_MEMFD_H +#include +#endif + +#include +#include + +#include "blockdev-util.h" +#include "btrfs-util.h" +#include "bus-common-errors.h" +#include "env-util.h" +#include "errno-list.h" +#include "errno-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "home-util.h" +#include "homed-home-bus.h" +#include "homed-home.h" +#include "missing.h" +#include "mkdir.h" +#include "path-util.h" +#include "process-util.h" +#include "pwquality-util.h" +#include "resize-fs.h" +#include "set.h" +#include "signal-util.h" +#include "stat-util.h" +#include "string-table.h" +#include "strv.h" +#include "user-record-sign.h" +#include "user-record-util.h" +#include "user-record.h" +#include "user-util.h" + +#define HOME_USERS_MAX 500 +#define PENDING_OPERATIONS_MAX 100 + +assert_cc(HOME_UID_MIN <= HOME_UID_MAX); +assert_cc(HOME_USERS_MAX <= (HOME_UID_MAX - HOME_UID_MIN + 1)); + +static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord *secret); + +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(operation_hash_ops, void, trivial_hash_func, trivial_compare_func, Operation, operation_unref); + +static int suitable_home_record(UserRecord *hr) { + int r; + + assert(hr); + + if (!hr->user_name) + return -EUNATCH; + + /* We are a bit more restrictive with what we accept as homed-managed user than what we accept in + * home records in general. Let's enforce the stricter rule here. */ + if (!suitable_user_name(hr->user_name)) + return -EINVAL; + if (!uid_is_valid(hr->uid)) + return -EINVAL; + + /* Insist we are outside of the dynamic and system range */ + if (uid_is_system(hr->uid) || gid_is_system(user_record_gid(hr)) || + uid_is_dynamic(hr->uid) || gid_is_dynamic(user_record_gid(hr))) + return -EADDRNOTAVAIL; + + /* Insist that GID and UID match */ + if (user_record_gid(hr) != (gid_t) hr->uid) + return -EBADSLT; + + /* Similar for the realm */ + if (hr->realm) { + r = suitable_realm(hr->realm); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + } + + return 0; +} + +int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret) { + _cleanup_(home_freep) Home *home = NULL; + _cleanup_free_ char *nm = NULL, *ns = NULL; + int r; + + assert(m); + assert(hr); + + r = suitable_home_record(hr); + if (r < 0) + return r; + + if (hashmap_contains(m->homes_by_name, hr->user_name)) + return -EBUSY; + + if (hashmap_contains(m->homes_by_uid, UID_TO_PTR(hr->uid))) + return -EBUSY; + + if (sysfs && hashmap_contains(m->homes_by_sysfs, sysfs)) + return -EBUSY; + + if (hashmap_size(m->homes_by_name) >= HOME_USERS_MAX) + return -EUSERS; + + nm = strdup(hr->user_name); + if (!nm) + return -ENOMEM; + + if (sysfs) { + ns = strdup(sysfs); + if (!ns) + return -ENOMEM; + } + + home = new(Home, 1); + if (!home) + return -ENOMEM; + + *home = (Home) { + .manager = m, + .user_name = TAKE_PTR(nm), + .uid = hr->uid, + .state = _HOME_STATE_INVALID, + .worker_stdout_fd = -1, + .sysfs = TAKE_PTR(ns), + .signed_locally = -1, + }; + + r = hashmap_put(m->homes_by_name, home->user_name, home); + if (r < 0) + return r; + + r = hashmap_put(m->homes_by_uid, UID_TO_PTR(home->uid), home); + if (r < 0) + return r; + + if (home->sysfs) { + r = hashmap_put(m->homes_by_sysfs, home->sysfs, home); + if (r < 0) + return r; + } + + r = user_record_clone(hr, USER_RECORD_LOAD_MASK_SECRET, &home->record); + if (r < 0) + return r; + + (void) bus_manager_emit_auto_login_changed(m); + (void) bus_home_emit_change(home); + + if (ret) + *ret = TAKE_PTR(home); + else + TAKE_PTR(home); + + return 0; +} + +Home *home_free(Home *h) { + + if (!h) + return NULL; + + if (h->manager) { + (void) bus_home_emit_remove(h); + (void) bus_manager_emit_auto_login_changed(h->manager); + + if (h->user_name) + (void) hashmap_remove_value(h->manager->homes_by_name, h->user_name, h); + + if (uid_is_valid(h->uid)) + (void) hashmap_remove_value(h->manager->homes_by_uid, UID_TO_PTR(h->uid), h); + + if (h->sysfs) + (void) hashmap_remove_value(h->manager->homes_by_sysfs, h->sysfs, h); + + if (h->worker_pid > 0) + (void) hashmap_remove_value(h->manager->homes_by_worker_pid, PID_TO_PTR(h->worker_pid), h); + + if (h->manager->gc_focus == h) + h->manager->gc_focus = NULL; + } + + user_record_unref(h->record); + user_record_unref(h->secret); + + h->worker_event_source = sd_event_source_unref(h->worker_event_source); + safe_close(h->worker_stdout_fd); + free(h->user_name); + free(h->sysfs); + + h->ref_event_source_please_suspend = sd_event_source_unref(h->ref_event_source_please_suspend); + h->ref_event_source_dont_suspend = sd_event_source_unref(h->ref_event_source_dont_suspend); + + h->pending_operations = ordered_set_free(h->pending_operations); + h->pending_event_source = sd_event_source_unref(h->pending_event_source); + h->deferred_change_event_source = sd_event_source_unref(h->deferred_change_event_source); + + h->current_operation = operation_unref(h->current_operation); + + return mfree(h); +} + +int home_set_record(Home *h, UserRecord *hr) { + _cleanup_(user_record_unrefp) UserRecord *new_hr = NULL; + Home *other; + int r; + + assert(h); + assert(h->user_name); + assert(h->record); + assert(hr); + + if (user_record_equal(h->record, hr)) + return 0; + + r = suitable_home_record(hr); + if (r < 0) + return r; + + if (!user_record_compatible(h->record, hr)) + return -EREMCHG; + + if (!FLAGS_SET(hr->mask, USER_RECORD_REGULAR) || + FLAGS_SET(hr->mask, USER_RECORD_SECRET)) + return -EINVAL; + + if (FLAGS_SET(h->record->mask, USER_RECORD_STATUS)) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + + /* Hmm, the existing record has status fields? If so, copy them over */ + + v = json_variant_ref(hr->json); + r = json_variant_set_field(&v, "status", json_variant_by_key(h->record->json, "status")); + if (r < 0) + return r; + + new_hr = user_record_new(); + if (!new_hr) + return -ENOMEM; + + r = user_record_load(new_hr, v, USER_RECORD_LOAD_REFUSE_SECRET); + if (r < 0) + return r; + + hr = new_hr; + } + + other = hashmap_get(h->manager->homes_by_uid, UID_TO_PTR(hr->uid)); + if (other && other != h) + return -EBUSY; + + if (h->uid != hr->uid) { + r = hashmap_remove_and_replace(h->manager->homes_by_uid, UID_TO_PTR(h->uid), UID_TO_PTR(hr->uid), h); + if (r < 0) + return r; + } + + user_record_unref(h->record); + h->record = user_record_ref(hr); + h->uid = h->record->uid; + + /* The updated record might have a different autologin setting, trigger a PropertiesChanged event for it */ + (void) bus_manager_emit_auto_login_changed(h->manager); + (void) bus_home_emit_change(h); + + return 0; +} + +int home_save_record(Home *h) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_free_ char *text = NULL; + const char *fn; + int r; + + assert(h); + + v = json_variant_ref(h->record->json); + r = json_variant_normalize(&v); + if (r < 0) + log_warning_errno(r, "User record could not be normalized."); + + r = json_variant_format(v, JSON_FORMAT_PRETTY|JSON_FORMAT_NEWLINE, &text); + if (r < 0) + return r; + + (void) mkdir("/var/lib/systemd/", 0755); + (void) mkdir("/var/lib/systemd/home/", 0700); + + fn = strjoina("/var/lib/systemd/home/", h->user_name, ".identity"); + + r = write_string_file(fn, text, WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MODE_0600); + if (r < 0) + return r; + + return 0; +} + +int home_unlink_record(Home *h) { + const char *fn; + + assert(h); + + fn = strjoina("/var/lib/systemd/home/", h->user_name, ".identity"); + if (unlink(fn) < 0 && errno != ENOENT) + return -errno; + + fn = strjoina("/run/systemd/home/", h->user_name, ".ref"); + if (unlink(fn) < 0 && errno != ENOENT) + return -errno; + + return 0; +} + +static void home_set_state(Home *h, HomeState state) { + HomeState old_state, new_state; + + assert(h); + + old_state = home_get_state(h); + h->state = state; + new_state = home_get_state(h); /* Query the new state, since the 'state' variable might be set to -1, + * in which case we synthesize an high-level state on demand */ + + log_info("%s: changing state %s → %s", h->user_name, + home_state_to_string(old_state), + home_state_to_string(new_state)); + + if (HOME_STATE_IS_EXECUTING_OPERATION(old_state) && !HOME_STATE_IS_EXECUTING_OPERATION(new_state)) { + /* If we just finished executing some operation, process the queue of pending operations. And + * enqueue it for GC too. */ + + home_schedule_operation(h, NULL, NULL); + manager_enqueue_gc(h->manager, h); + } +} + +static int home_parse_worker_stdout(int _fd, UserRecord **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_close_ int fd = _fd; /* take possession, even on failure */ + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + _cleanup_fclose_ FILE *f = NULL; + unsigned line, column; + struct stat st; + int r; + + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to stat stdout fd: %m"); + + assert(S_ISREG(st.st_mode)); + + if (st.st_size == 0) { /* empty record */ + *ret = NULL; + return 0; + } + + if (lseek(fd, SEEK_SET, 0) == (off_t) -1) + return log_error_errno(errno, "Failed to seek to beginning of memfd: %m"); + + f = fdopen(fd, "r"); + if (!f) + return log_error_errno(errno, "Failed to reopen memfd: %m"); + + TAKE_FD(fd); + + if (DEBUG_LOGGING) { + _cleanup_free_ char *text = NULL; + + r = read_full_stream(f, &text, NULL); + if (r < 0) + return log_error_errno(r, "Failed to read from client: %m"); + + log_debug("Got from worker: %s", text); + rewind(f); + } + + r = json_parse_file(f, "stdout", JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse identity at %u:%u: %m", line, column); + + hr = user_record_new(); + if (!hr) + return log_oom(); + + r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET); + if (r < 0) + return log_error_errno(r, "Failed to load home record identity: %m"); + + *ret = TAKE_PTR(hr); + return 1; +} + +static int home_verify_user_record(Home *h, UserRecord *hr, bool *ret_signed_locally, sd_bus_error *ret_error) { + int is_signed; + + assert(h); + assert(hr); + assert(ret_signed_locally); + + is_signed = manager_verify_user_record(h->manager, hr); + switch (is_signed) { + + case USER_RECORD_SIGNED_EXCLUSIVE: + log_info("Home %s is signed exclusively by our key, accepting.", hr->user_name); + *ret_signed_locally = true; + return 0; + + case USER_RECORD_SIGNED: + log_info("Home %s is signed by our key (and others), accepting.", hr->user_name); + *ret_signed_locally = false; + return 0; + + case USER_RECORD_FOREIGN: + log_info("Home %s is signed by foreign key we like, accepting.", hr->user_name); + *ret_signed_locally = false; + return 0; + + case USER_RECORD_UNSIGNED: + sd_bus_error_setf(ret_error, BUS_ERROR_BAD_SIGNATURE, "User record %s is not signed at all, refusing.", hr->user_name); + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Home %s contains user record that is not signed at all, refusing.", hr->user_name); + + case -ENOKEY: + sd_bus_error_setf(ret_error, BUS_ERROR_BAD_SIGNATURE, "User record %s is not signed by any known key, refusing.", hr->user_name); + return log_error_errno(is_signed, "Home %s contians user record that is not signed by any known key, refusing.", hr->user_name); + + default: + assert(is_signed < 0); + return log_error_errno(is_signed, "Failed to verify signature on user record for %s, refusing fixation: %m", hr->user_name); + } +} + +static int convert_worker_errno(Home *h, int e, sd_bus_error *error) { + /* Converts the error numbers the worker process returned into somewhat sensible dbus errors */ + + switch (e) { + + case -EMSGSIZE: + return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "File systems of this type cannot shrinked"); + case -ETXTBSY: + return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "File systems of this type can only be shrinked offline"); + case -ERANGE: + return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "File system size too small"); + case -ENOLINK: + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "System does not support selected storage backend"); + case -EPROTONOSUPPORT: + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "System does not support selected file system"); + case -ENOTTY: + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Operation not supported on storage backend"); + case -ESOCKTNOSUPPORT: + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Operation not supported on file system"); + case -ENOKEY: + return sd_bus_error_setf(error, BUS_ERROR_BAD_PASSWORD, "Password for home %s is incorrect or not sufficient for authentication.", h->user_name); + case -EBUSY: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name); + case -ENOEXEC: + return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s is currently not active", h->user_name); + case -ENOSPC: + return sd_bus_error_setf(error, BUS_ERROR_NO_DISK_SPACE, "Not enough disk space for home %s", h->user_name); + } + + return 0; +} + +static void home_count_bad_authentication(Home *h, bool save) { + int r; + + assert(h); + + r = user_record_bad_authentication(h->record); + if (r < 0) { + log_warning_errno(r, "Failed to increase bad authentication counter, ignoring: %m"); + return; + } + + if (save) { + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to write home record to disk, ignoring: %m"); + } +} + +static void home_fixate_finish(Home *h, int ret, UserRecord *hr) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(user_record_unrefp) UserRecord *secret = NULL; + bool signed_locally; + int r; + + assert(h); + assert(IN_SET(h->state, HOME_FIXATING, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE)); + + secret = TAKE_PTR(h->secret); /* Take possession */ + + if (ret < 0) { + if (ret == -ENOKEY) + (void) home_count_bad_authentication(h, false); + + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Fixation failed: %m"); + goto fail; + } + if (!hr) { + r = log_error_errno(SYNTHETIC_ERRNO(EIO), "Did not receive user record from worker process, fixation failed."); + goto fail; + } + + r = home_verify_user_record(h, hr, &signed_locally, &error); + if (r < 0) + goto fail; + + r = home_set_record(h, hr); + if (r < 0) { + log_error_errno(r, "Failed to update home record: %m"); + goto fail; + } + + h->signed_locally = signed_locally; + + /* When we finished fixating (and don't follow-up with activation), let's count this as good authentication */ + if (h->state == HOME_FIXATING) { + r = user_record_good_authentication(h->record); + if (r < 0) + log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m"); + } + + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to write home record to disk, ignoring: %m"); + + if (IN_SET(h->state, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE)) { + + r = home_start_work(h, "activate", h->record, secret); + if (r < 0) { + h->current_operation = operation_result_unref(h->current_operation, r, NULL); + home_set_state(h, _HOME_STATE_INVALID); + } else + home_set_state(h, h->state == HOME_FIXATING_FOR_ACTIVATION ? HOME_ACTIVATING : HOME_ACTIVATING_FOR_ACQUIRE); + + return; + } + + log_debug("Fixation of %s completed.", h->user_name); + + h->current_operation = operation_result_unref(h->current_operation, 0, NULL); + + /* Reset the state to "invalid", which makes home_get_state() test if the image exists and returns + * HOME_ABSENT vs. HOME_INACTIVE as necessary. */ + home_set_state(h, _HOME_STATE_INVALID); + return; + +fail: + /* If fixation fails, we stay in unfixated state! */ + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, HOME_UNFIXATED); +} + +static void home_activate_finish(Home *h, int ret, UserRecord *hr) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(IN_SET(h->state, HOME_ACTIVATING, HOME_ACTIVATING_FOR_ACQUIRE)); + + if (ret < 0) { + if (ret == -ENOKEY) + home_count_bad_authentication(h, true); + + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Activation failed: %m"); + goto finish; + } + + if (hr) { + bool signed_locally; + + r = home_verify_user_record(h, hr, &signed_locally, &error); + if (r < 0) + goto finish; + + r = home_set_record(h, hr); + if (r < 0) { + log_error_errno(r, "Failed to update home record, ignoring: %m"); + goto finish; + } + + h->signed_locally = signed_locally; + + r = user_record_good_authentication(h->record); + if (r < 0) + log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m"); + + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to write home record to disk, ignoring: %m"); + } + + log_debug("Activation of %s completed.", h->user_name); + r = 0; + +finish: + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_deactivate_finish(Home *h, int ret, UserRecord *hr) { + sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(h->state == HOME_DEACTIVATING); + assert(!hr); /* We don't expect a record on this operation */ + + if (ret < 0) { + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Deactivation of %s failed: %m", h->user_name); + goto finish; + } + + log_debug("Deactivation of %s completed.", h->user_name); + r = 0; + +finish: + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_remove_finish(Home *h, int ret, UserRecord *hr) { + sd_bus_error error = SD_BUS_ERROR_NULL; + Manager *m; + int r; + + assert(h); + assert(h->state == HOME_REMOVING); + assert(!hr); /* We don't expect a record on this operation */ + + m = h->manager; + + if (ret < 0 && ret != -EALREADY) { + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Removing %s failed: %m", h->user_name); + goto fail; + } + + /* For a couple of storage types we can't delete the actual data storage when called (such as LUKS on + * partitions like USB sticks, or so). Sometimes these storage locations are among those we normally + * automatically discover in /home or in udev. When such a home is deleted let's hence issue a rescan + * after completion, so that "unfixated" entries are rediscovered. */ + if (!IN_SET(user_record_test_image_path(h->record), USER_TEST_UNDEFINED, USER_TEST_ABSENT)) + manager_enqueue_rescan(m); + + /* The image is now removed from disk. Now also remove our stored record */ + r = home_unlink_record(h); + if (r < 0) { + log_error_errno(r, "Removing record file failed: %m"); + goto fail; + } + + log_debug("Removal of %s completed.", h->user_name); + h->current_operation = operation_result_unref(h->current_operation, 0, NULL); + + /* Unload this record from memory too now. */ + h = home_free(h); + return; + +fail: + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_create_finish(Home *h, int ret, UserRecord *hr) { + int r; + + assert(h); + assert(h->state == HOME_CREATING); + + if (ret < 0) { + sd_bus_error error = SD_BUS_ERROR_NULL; + + (void) convert_worker_errno(h, ret, &error); + log_error_errno(ret, "Operation on %s failed: %m", h->user_name); + h->current_operation = operation_result_unref(h->current_operation, ret, &error); + + if (h->unregister_on_failure) { + (void) home_unlink_record(h); + h = home_free(h); + return; + } + + home_set_state(h, _HOME_STATE_INVALID); + return; + } + + if (hr) { + r = home_set_record(h, hr); + if (r < 0) + log_warning_errno(r, "Failed to update home record, ignoring: %m"); + } + + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to save record to disk, ignoring: %m"); + + log_debug("Creation of %s completed.", h->user_name); + + h->current_operation = operation_result_unref(h->current_operation, 0, NULL); + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_change_finish(Home *h, int ret, UserRecord *hr) { + sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + + if (ret < 0) { + if (ret == -ENOKEY) + (void) home_count_bad_authentication(h, true); + + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Change operation failed: %m"); + goto finish; + } + + if (hr) { + r = home_set_record(h, hr); + if (r < 0) + log_warning_errno(r, "Failed to update home record, ignoring: %m"); + else { + r = user_record_good_authentication(h->record); + if (r < 0) + log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m"); + + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to write home record to disk, ignoring: %m"); + } + } + + log_debug("Change operation of %s completed.", h->user_name); + r = 0; + +finish: + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_locking_finish(Home *h, int ret, UserRecord *hr) { + sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(h->state == HOME_LOCKING); + + if (ret < 0) { + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Locking operation failed: %m"); + goto finish; + } + + log_debug("Locking operation of %s completed.", h->user_name); + h->current_operation = operation_result_unref(h->current_operation, 0, NULL); + home_set_state(h, HOME_LOCKED); + return; + +finish: + /* If a specific home doesn't know the concept of locking, then that's totally OK, don't propagate + * the error if we are executing a LockAllHomes() operation. */ + + if (h->current_operation->type == OPERATION_LOCK_ALL && r == -ENOTTY) + h->current_operation = operation_result_unref(h->current_operation, 0, NULL); + else + h->current_operation = operation_result_unref(h->current_operation, r, &error); + + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_unlocking_finish(Home *h, int ret, UserRecord *hr) { + sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(IN_SET(h->state, HOME_UNLOCKING, HOME_UNLOCKING_FOR_ACQUIRE)); + + if (ret < 0) { + if (ret == -ENOKEY) + (void) home_count_bad_authentication(h, true); + + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Unlocking operation failed: %m"); + goto finish; + } + + r = user_record_good_authentication(h->record); + if (r < 0) + log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m"); + else { + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to write home record to disk, ignoring: %m"); + } + + log_debug("Unlocking operation of %s completed.", h->user_name); + r = 0; + +finish: + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, _HOME_STATE_INVALID); +} + +static void home_authenticating_finish(Home *h, int ret, UserRecord *hr) { + sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(IN_SET(h->state, HOME_AUTHENTICATING, HOME_AUTHENTICATING_WHILE_ACTIVE, HOME_AUTHENTICATING_FOR_ACQUIRE)); + + if (ret < 0) { + if (ret == -ENOKEY) + (void) home_count_bad_authentication(h, true); + + (void) convert_worker_errno(h, ret, &error); + r = log_error_errno(ret, "Authentication failed: %m"); + goto finish; + } + + if (hr) { + r = home_set_record(h, hr); + if (r < 0) + log_warning_errno(r, "Failed to update home record, ignoring: %m"); + else { + r = user_record_good_authentication(h->record); + if (r < 0) + log_warning_errno(r, "Failed to increase good authentication counter, ignoring: %m"); + + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to write home record to disk, ignoring: %m"); + } + } + + log_debug("Authentication of %s completed.", h->user_name); + r = 0; + +finish: + h->current_operation = operation_result_unref(h->current_operation, r, &error); + home_set_state(h, _HOME_STATE_INVALID); +} + +static int home_on_worker_process(sd_event_source *s, const siginfo_t *si, void *userdata) { + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + _cleanup_fclose_ FILE *f = NULL; + Home *h = userdata; + int ret; + + assert(s); + assert(si); + assert(h); + + assert(h->worker_pid == si->si_pid); + assert(h->worker_event_source); + assert(h->worker_stdout_fd >= 0); + + (void) hashmap_remove_value(h->manager->homes_by_worker_pid, PID_TO_PTR(h->worker_pid), h); + + h->worker_pid = 0; + h->worker_event_source = sd_event_source_unref(h->worker_event_source); + + if (si->si_code != CLD_EXITED) { + assert(IN_SET(si->si_code, CLD_KILLED, CLD_DUMPED)); + ret = log_debug_errno(SYNTHETIC_ERRNO(EPROTO), "Worker process died abnormally with signal %s.", signal_to_string(si->si_status)); + } else if (si->si_status != EXIT_SUCCESS) { + /* If we received an error code via sd_notify(), use it */ + if (h->worker_error_code != 0) + ret = log_debug_errno(h->worker_error_code, "Worker reported error code %s.", errno_to_name(h->worker_error_code)); + else + ret = log_debug_errno(SYNTHETIC_ERRNO(EPROTO), "Worker exited with exit code %i.", si->si_status); + } else + ret = home_parse_worker_stdout(TAKE_FD(h->worker_stdout_fd), &hr); + + h->worker_stdout_fd = safe_close(h->worker_stdout_fd); + + switch (h->state) { + + case HOME_FIXATING: + case HOME_FIXATING_FOR_ACTIVATION: + case HOME_FIXATING_FOR_ACQUIRE: + home_fixate_finish(h, ret, hr); + break; + + case HOME_ACTIVATING: + case HOME_ACTIVATING_FOR_ACQUIRE: + home_activate_finish(h, ret, hr); + break; + + case HOME_DEACTIVATING: + home_deactivate_finish(h, ret, hr); + break; + + case HOME_LOCKING: + home_locking_finish(h, ret, hr); + break; + + case HOME_UNLOCKING: + case HOME_UNLOCKING_FOR_ACQUIRE: + home_unlocking_finish(h, ret, hr); + break; + + case HOME_CREATING: + home_create_finish(h, ret, hr); + break; + + case HOME_REMOVING: + home_remove_finish(h, ret, hr); + break; + + case HOME_UPDATING: + case HOME_UPDATING_WHILE_ACTIVE: + case HOME_RESIZING: + case HOME_RESIZING_WHILE_ACTIVE: + case HOME_PASSWD: + case HOME_PASSWD_WHILE_ACTIVE: + home_change_finish(h, ret, hr); + break; + + case HOME_AUTHENTICATING: + case HOME_AUTHENTICATING_WHILE_ACTIVE: + case HOME_AUTHENTICATING_FOR_ACQUIRE: + home_authenticating_finish(h, ret, hr); + break; + + default: + assert_not_reached("Unexpected state after worker exited"); + } + + return 0; +} + +static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord *secret) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(erase_and_freep) char *formatted = NULL; + _cleanup_close_ int stdin_fd = -1, stdout_fd = -1; + pid_t pid = 0; + int r; + + assert(h); + assert(verb); + assert(hr); + + if (h->worker_pid != 0) + return -EBUSY; + + assert(h->worker_stdout_fd < 0); + assert(!h->worker_event_source); + + v = json_variant_ref(hr->json); + + if (secret) { + JsonVariant *sub = NULL; + + sub = json_variant_by_key(secret->json, "secret"); + if (!sub) + return -ENOKEY; + + r = json_variant_set_field(&v, "secret", sub); + if (r < 0) + return r; + } + + r = json_variant_format(v, 0, &formatted); + if (r < 0) + return r; + + stdin_fd = acquire_data_fd(formatted, strlen(formatted), 0); + if (stdin_fd < 0) + return stdin_fd; + + log_debug("Sending to worker: %s", formatted); + + stdout_fd = memfd_create("homework-stdout", MFD_CLOEXEC); + if (stdout_fd < 0) + return -errno; + + r = safe_fork_full("(sd-homework)", + (int[]) { stdin_fd, stdout_fd }, 2, + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_LOG, &pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + + if (setenv("NOTIFY_SOCKET", "/run/systemd/home/notify", 1) < 0) { + log_error_errno(errno, "Failed to set $NOTIFY_SOCKET: %m"); + _exit(EXIT_FAILURE); + } + + r = rearrange_stdio(stdin_fd, stdout_fd, STDERR_FILENO); + if (r < 0) { + log_error_errno(r, "Failed to rearrange stdin/stdout/stderr: %m"); + _exit(EXIT_FAILURE); + } + + stdin_fd = stdout_fd = -1; /* have been invalidated by rearrange_stdio() */ + + // FIXME: drop this line + /* execl("/usr/bin/valgrind", "/usr/bin/valgrind", "/home/lennart/projects/systemd/build/systemd-homework", */ + /* /\* SYSTEMD_HOMEWORK_PATH,*\/ verb, NULL); */ + execl("/home/lennart/projects/systemd/build/systemd-homework", + SYSTEMD_HOMEWORK_PATH, verb, NULL); + + execl(SYSTEMD_HOMEWORK_PATH, SYSTEMD_HOMEWORK_PATH, verb, NULL); + log_error_errno(errno, "Failed to invoke " SYSTEMD_HOMEWORK_PATH ": %m"); + _exit(EXIT_FAILURE); + } + + r = sd_event_add_child(h->manager->event, &h->worker_event_source, pid, WEXITED, home_on_worker_process, h); + if (r < 0) + return r; + + (void) sd_event_source_set_description(h->worker_event_source, "worker"); + + r = hashmap_put(h->manager->homes_by_worker_pid, PID_TO_PTR(pid), h); + if (r < 0) { + h->worker_event_source = sd_event_source_unref(h->worker_event_source); + return r; + } + + h->worker_stdout_fd = TAKE_FD(stdout_fd); + h->worker_pid = pid; + h->worker_error_code = 0; + + return 0; +} + +static int home_ratelimit(Home *h, sd_bus_error *error) { + int r, ret; + + assert(h); + + ret = user_record_ratelimit(h->record); + if (ret < 0) + return ret; + + if (h->state != HOME_UNFIXATED) { + r = home_save_record(h); + if (r < 0) + log_warning_errno(r, "Failed to save updated record, ignoring: %m"); + } + + if (ret == 0) { + char buf[FORMAT_TIMESPAN_MAX]; + usec_t t, n; + + n = now(CLOCK_REALTIME); + t = user_record_ratelimit_next_try(h->record); + + if (t != USEC_INFINITY && t > n) + return sd_bus_error_setf(error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT, "Too many login attempts, please try again in %s!", + format_timespan(buf, sizeof(buf), t - n, USEC_PER_SEC)); + + return sd_bus_error_setf(error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT, "Too many login attempts, please try again later."); + } + + return 0; +} + +static int home_fixate_internal( + Home *h, + UserRecord *secret, + HomeState for_state, + sd_bus_error *error) { + + int r; + + assert(h); + assert(IN_SET(for_state, HOME_FIXATING, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE)); + + r = home_start_work(h, "inspect", h->record, secret); + if (r < 0) + return r; + + if (for_state == HOME_FIXATING_FOR_ACTIVATION) { + /* Remember the secret data, since we need it for the activation again, later on. */ + user_record_unref(h->secret); + h->secret = user_record_ref(secret); + } + + home_set_state(h, for_state); + return 0; +} + +int home_fixate(Home *h, UserRecord *secret, sd_bus_error *error) { + int r; + + assert(h); + + switch (home_get_state(h)) { + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_INACTIVE: + case HOME_ACTIVE: + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ALREADY_FIXATED, "Home %s is already fixated.", h->user_name); + case HOME_UNFIXATED: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + return home_fixate_internal(h, secret, HOME_FIXATING, error); +} + +static int home_activate_internal(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) { + int r; + + assert(h); + assert(IN_SET(for_state, HOME_ACTIVATING, HOME_ACTIVATING_FOR_ACQUIRE)); + + r = home_start_work(h, "activate", h->record, secret); + if (r < 0) + return r; + + home_set_state(h, for_state); + return 0; +} + +int home_activate(Home *h, UserRecord *secret, sd_bus_error *error) { + int r; + + assert(h); + + switch (home_get_state(h)) { + case HOME_UNFIXATED: + return home_fixate_internal(h, secret, HOME_FIXATING_FOR_ACTIVATION, error); + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_ACTIVE: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ALREADY_ACTIVE, "Home %s is already active.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_INACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + return home_activate_internal(h, secret, HOME_ACTIVATING, error); +} + +static int home_authenticate_internal(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) { + int r; + + assert(h); + assert(IN_SET(for_state, HOME_AUTHENTICATING, HOME_AUTHENTICATING_WHILE_ACTIVE, HOME_AUTHENTICATING_FOR_ACQUIRE)); + + r = home_start_work(h, "inspect", h->record, secret); + if (r < 0) + return r; + + home_set_state(h, for_state); + return 0; +} + +int home_authenticate(Home *h, UserRecord *secret, sd_bus_error *error) { + HomeState state; + int r; + + assert(h); + + state = home_get_state(h); + switch (state) { + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_UNFIXATED: + case HOME_INACTIVE: + case HOME_ACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + return home_authenticate_internal(h, secret, state == HOME_ACTIVE ? HOME_AUTHENTICATING_WHILE_ACTIVE : HOME_AUTHENTICATING, error); +} + +static int home_deactivate_internal(Home *h, bool force, sd_bus_error *error) { + int r; + + assert(h); + + r = home_start_work(h, force ? "deactivate-force" : "deactivate", h->record, NULL); + if (r < 0) + return r; + + home_set_state(h, HOME_DEACTIVATING); + return 0; +} + +int home_deactivate(Home *h, bool force, sd_bus_error *error) { + assert(h); + + switch (home_get_state(h)) { + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s not active.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_ACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + return home_deactivate_internal(h, force, error); +} + +int home_create(Home *h, UserRecord *secret, sd_bus_error *error) { + int r; + + assert(h); + + switch (home_get_state(h)) { + case HOME_INACTIVE: + if (h->record->storage < 0) + break; /* if no storage is defined we don't know what precisely to look for, hence + * HOME_INACTIVE is OK in that case too. */ + + if (IN_SET(user_record_test_image_path(h->record), USER_TEST_MAYBE, USER_TEST_UNDEFINED)) + break; /* And if the image path test isn't conclusive, let's also go on */ + + _fallthrough_; + case HOME_UNFIXATED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_EXISTS, "Home of user %s already exists.", h->user_name); + case HOME_ABSENT: + break; + case HOME_ACTIVE: + case HOME_LOCKED: + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name); + } + + if (h->record->enforce_password_policy == false) + log_debug("Password quality check turned off for account, skipping."); + else { + r = quality_check_password(h->record, secret, error); + if (r < 0) + return r; + } + + r = home_start_work(h, "create", h->record, secret); + if (r < 0) + return r; + + home_set_state(h, HOME_CREATING); + return 0; +} + +int home_remove(Home *h, sd_bus_error *error) { + HomeState state; + int r; + + assert(h); + + state = home_get_state(h); + switch (state) { + case HOME_ABSENT: /* If the home directory is absent, then this is just like unregistering */ + return home_unregister(h, error); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_UNFIXATED: + case HOME_INACTIVE: + break; + case HOME_ACTIVE: + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name); + } + + r = home_start_work(h, "remove", h->record, NULL); + if (r < 0) + return r; + + home_set_state(h, HOME_REMOVING); + return 0; +} + +static int user_record_extend_with_binding(UserRecord *hr, UserRecord *with_binding, UserRecordLoadFlags flags, UserRecord **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *nr = NULL; + JsonVariant *binding; + int r; + + assert(hr); + assert(with_binding); + assert(ret); + + assert_se(v = json_variant_ref(hr->json)); + + binding = json_variant_by_key(with_binding->json, "binding"); + if (binding) { + r = json_variant_set_field(&v, "binding", binding); + if (r < 0) + return r; + } + + nr = user_record_new(); + if (!nr) + return -ENOMEM; + + r = user_record_load(nr, v, flags); + if (r < 0) + return r; + + *ret = TAKE_PTR(nr); + return 0; +} + +static int home_update_internal(Home *h, const char *verb, UserRecord *hr, UserRecord *secret, sd_bus_error *error) { + _cleanup_(user_record_unrefp) UserRecord *new_hr = NULL, *saved_secret = NULL, *signed_hr = NULL; + int r, c; + + assert(h); + assert(verb); + assert(hr); + + if (!user_record_compatible(hr, h->record)) + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Updated user record is not compatible with existing one."); + c = user_record_compare_last_change(hr, h->record); /* refuse downgrades */ + if (c < 0) + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_DOWNGRADE, "Refusing to update to older home record."); + + if (!secret && FLAGS_SET(hr->mask, USER_RECORD_SECRET)) { + r = user_record_clone(hr, USER_RECORD_EXTRACT_SECRET, &saved_secret); + if (r < 0) + return r; + + secret = saved_secret; + } + + r = manager_verify_user_record(h->manager, hr); + switch (r) { + + case USER_RECORD_UNSIGNED: + if (h->signed_locally <= 0) /* If the existing record is not owned by us, don't accept an + * unsigned new record. i.e. only implicitly sign new records + * that where previously signed by us too. */ + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_SIGNED, "Home %s is signed and cannot be modified locally.", h->user_name); + + /* The updated record is not signed, then do so now */ + r = manager_sign_user_record(h->manager, hr, &signed_hr, error); + if (r < 0) + return r; + + hr = signed_hr; + break; + + case USER_RECORD_SIGNED_EXCLUSIVE: + case USER_RECORD_SIGNED: + case USER_RECORD_FOREIGN: + /* Has already been signed. Great! */ + break; + + case -ENOKEY: + default: + return r; + } + + r = user_record_extend_with_binding(hr, h->record, USER_RECORD_LOAD_MASK_SECRET, &new_hr); + if (r < 0) + return r; + + if (c == 0) { + /* different payload but same lastChangeUSec field? That's not cool! */ + + r = user_record_masked_equal(new_hr, h->record, USER_RECORD_REGULAR|USER_RECORD_PRIVILEGED|USER_RECORD_PER_MACHINE); + if (r < 0) + return r; + if (r == 0) + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Home record different but timestamp remained the same, refusing."); + } + + r = home_start_work(h, verb, new_hr, secret); + if (r < 0) + return r; + + return 0; +} + +int home_update(Home *h, UserRecord *hr, sd_bus_error *error) { + HomeState state; + int r; + + assert(h); + assert(hr); + + state = home_get_state(h); + switch (state) { + case HOME_UNFIXATED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s has not been fixated yet.", h->user_name); + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_INACTIVE: + case HOME_ACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + r = home_update_internal(h, "update", hr, NULL, error); + if (r < 0) + return r; + + home_set_state(h, state == HOME_ACTIVE ? HOME_UPDATING_WHILE_ACTIVE : HOME_UPDATING); + return 0; +} + +int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, sd_bus_error *error) { + _cleanup_(user_record_unrefp) UserRecord *c = NULL; + HomeState state; + int r; + + assert(h); + + state = home_get_state(h); + switch (state) { + case HOME_UNFIXATED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s has not been fixated yet.", h->user_name); + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_INACTIVE: + case HOME_ACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + if (disk_size == UINT64_MAX || disk_size == h->record->disk_size) { + if (h->record->disk_size == UINT64_MAX) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Not disk size to resize to specified."); + + c = user_record_ref(h->record); /* Shortcut if size is unspecified or matches the record */ + } else { + _cleanup_(user_record_unrefp) UserRecord *signed_c = NULL; + + if (h->signed_locally <= 0) /* Don't allow changing of records not signed only by us */ + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_SIGNED, "Home %s is signed and cannot be modified locally.", h->user_name); + + r = user_record_clone(h->record, USER_RECORD_LOAD_REFUSE_SECRET, &c); + if (r < 0) + return r; + + r = user_record_set_disk_size(c, disk_size); + if (r == -ERANGE) + return sd_bus_error_setf(error, BUS_ERROR_BAD_HOME_SIZE, "Requested size for home %s out of acceptable range.", h->user_name); + if (r < 0) + return r; + + r = user_record_update_last_changed(c, false); + if (r == -ECHRNG) + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Record last change time of %s is newer than current time, cannot update.", h->user_name); + if (r < 0) + return r; + + r = manager_sign_user_record(h->manager, c, &signed_c, error); + if (r < 0) + return r; + + user_record_unref(c); + c = TAKE_PTR(signed_c); + } + + r = home_update_internal(h, "resize", c, secret, error); + if (r < 0) + return r; + + home_set_state(h, state == HOME_ACTIVE ? HOME_RESIZING_WHILE_ACTIVE : HOME_RESIZING); + return 0; +} + +int home_passwd(Home *h, + UserRecord *new_secret, + UserRecord *old_secret, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *c = NULL, *merged_secret = NULL, *signed_c = NULL; + HomeState state; + int r; + + assert(h); + + if (h->signed_locally <= 0) /* Don't allow changing of records not signed only by us */ + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_SIGNED, "Home %s is signed and cannot be modified locally.", h->user_name); + if (strv_isempty(new_secret->password)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "No password to set specified."); + + state = home_get_state(h); + switch (state) { + case HOME_UNFIXATED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s has not been fixated yet.", h->user_name); + case HOME_ABSENT: + return sd_bus_error_setf(error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_INACTIVE: + case HOME_ACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + r = user_record_clone(h->record, USER_RECORD_LOAD_REFUSE_SECRET, &c); + if (r < 0) + return r; + + merged_secret = user_record_new(); + if (!merged_secret) + return -ENOMEM; + + r = user_record_merge_secret(merged_secret, old_secret); + if (r < 0) + return r; + + r = user_record_merge_secret(merged_secret, new_secret); + if (r < 0) + return r; + + r = user_record_make_hashed_password(c, new_secret); + if (r < 0) + return r; + + r = user_record_update_last_changed(c, true); + if (r == -ECHRNG) + return sd_bus_error_setf(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Record last change time of %s is newer than current time, cannot update.", h->user_name); + if (r < 0) + return r; + + r = manager_sign_user_record(h->manager, c, &signed_c, error); + if (r < 0) + return r; + + if (c->enforce_password_policy == false) + log_debug("Password quality check turned off for account, skipping."); + else { + r = quality_check_password(c, merged_secret, error); + if (r < 0) + return r; + } + + r = home_update_internal(h, "passwd", signed_c, merged_secret, error); + if (r < 0) + return r; + + home_set_state(h, state == HOME_ACTIVE ? HOME_PASSWD_WHILE_ACTIVE : HOME_PASSWD); + return 0; +} + +int home_unregister(Home *h, sd_bus_error *error) { + int r; + + assert(h); + + switch (home_get_state(h)) { + case HOME_UNFIXATED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_UNFIXATED, "Home %s is not registered.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + case HOME_ABSENT: + case HOME_INACTIVE: + break; + case HOME_ACTIVE: + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name); + } + + r = home_unlink_record(h); + if (r < 0) + return r; + + /* And destroy the whole entry. The caller needs to be prepared for that. */ + h = home_free(h); + return 1; +} + +int home_lock(Home *h, sd_bus_error *error) { + int r; + + assert(h); + + switch (home_get_state(h)) { + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_ACTIVE, "Home %s is not active.", h->user_name); + case HOME_LOCKED: + return sd_bus_error_setf(error, BUS_ERROR_HOME_LOCKED, "Home %s is already locked.", h->user_name); + case HOME_ACTIVE: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + r = home_start_work(h, "lock", h->record, NULL); + if (r < 0) + return r; + + home_set_state(h, HOME_LOCKING); + return 0; +} + +static int home_unlock_internal(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) { + int r; + + assert(h); + assert(IN_SET(for_state, HOME_UNLOCKING, HOME_UNLOCKING_FOR_ACQUIRE)); + + r = home_start_work(h, "unlock", h->record, secret); + if (r < 0) + return r; + + home_set_state(h, for_state); + return 0; +} + +int home_unlock(Home *h, UserRecord *secret, sd_bus_error *error) { + int r; + assert(h); + + r = home_ratelimit(h, error); + if (r < 0) + return r; + + switch (home_get_state(h)) { + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + case HOME_ACTIVE: + return sd_bus_error_setf(error, BUS_ERROR_HOME_NOT_LOCKED, "Home %s is not locked.", h->user_name); + case HOME_LOCKED: + break; + default: + return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); + } + + return home_unlock_internal(h, secret, HOME_UNLOCKING, error); +} + +HomeState home_get_state(Home *h) { + assert(h); + + /* When the state field is initialized, it counts. */ + if (h->state >= 0) + return h->state; + + /* Otherwise, let's see if the home directory is mounted. If so, we assume for sure the home + * directory is active */ + if (user_record_test_home_directory(h->record) == USER_TEST_MOUNTED) + return HOME_ACTIVE; + + /* And if we see the image being gone, we report this as absent */ + if (user_record_test_image_path(h->record) == USER_TEST_ABSENT) + return HOME_ABSENT; + + /* And for all other cases we return "inactive". */ + return HOME_INACTIVE; +} + +void home_process_notify(Home *h, char **l) { + const char *e; + int error; + int r; + + assert(h); + + e = strv_env_get(l, "ERRNO"); + if (!e) { + log_debug("Got notify message lacking ERRNO= field, ignoring."); + return; + } + + r = safe_atoi(e, &error); + if (r < 0) { + log_debug_errno(r, "Failed to parse receieved error number, ignoring: %s", e); + return; + } + if (error <= 0) { + log_debug("Error number is out of range: %i", error); + return; + } + + h->worker_error_code = error; +} + +int home_killall(Home *h) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_free_ char *unit = NULL; + int r; + + assert(h); + + if (!uid_is_valid(h->uid)) + return 0; + + assert(h->uid > 0); /* We never should be UID 0 */ + + /* Let's kill everything matching the specified UID */ + r = safe_fork("(sd-killer)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_WAIT|FORK_LOG, NULL); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + + if (setresgid(h->uid, h->uid, h->uid) < 0) { + log_error_errno(errno, "Failed to change GID to " GID_FMT ": %m", h->uid); + _exit(EXIT_FAILURE); + } + + if (setgroups(0, NULL) < 0) { + log_error_errno(errno, "Failed to reset auxiliary groups list: %m"); + _exit(EXIT_FAILURE); + } + + if (setresuid(h->uid, h->uid, h->uid) < 0) { + log_error_errno(errno, "Failed to change UID to " UID_FMT ": %m", h->uid); + _exit(EXIT_FAILURE); + } + + if (kill(-1, SIGKILL) < 0) { + log_error_errno(errno, "Failed to kill all processes of UID " UID_FMT ": %m", h->uid); + _exit(EXIT_FAILURE); + } + + _exit(EXIT_SUCCESS); + } + + /* Let's also kill everything in the user's slice */ + if (asprintf(&unit, "user-" UID_FMT ".slice", h->uid) < 0) + return log_oom(); + + r = sd_bus_call_method( + h->manager->bus, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + "KillUnit", + &error, + NULL, + "ssi", unit, "all", SIGKILL); + if (r < 0) + log_full_errno(sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_UNIT) ? LOG_DEBUG : LOG_WARNING, + r, "Failed to kill login processes of user, ignoring: %s", bus_error_message(&error, r)); + + return 1; +} + +static int home_get_disk_status_luks( + Home *h, + HomeState state, + uint64_t *ret_disk_size, + uint64_t *ret_disk_usage, + uint64_t *ret_disk_free, + uint64_t *ret_disk_ceiling, + uint64_t *ret_disk_floor) { + + uint64_t disk_size = UINT64_MAX, disk_usage = UINT64_MAX, disk_free = UINT64_MAX, + disk_ceiling = UINT64_MAX, disk_floor = UINT64_MAX, + stat_used = UINT64_MAX, fs_size = UINT64_MAX, header_size = 0; + + struct statfs sfs; + const char *hd; + int r; + + assert(h); + assert(ret_disk_size); + assert(ret_disk_usage); + assert(ret_disk_free); + assert(ret_disk_ceiling); + + if (state != HOME_ABSENT) { + const char *ip; + + ip = user_record_image_path(h->record); + if (ip) { + struct stat st; + + if (stat(ip, &st) < 0) + log_debug_errno(errno, "Failed to stat() %s, ignoring: %m", ip); + else if (S_ISREG(st.st_mode)) { + _cleanup_free_ char *parent = NULL; + + disk_size = st.st_size; + stat_used = st.st_blocks * 512; + + parent = dirname_malloc(ip); + if (!parent) + return log_oom(); + + if (statfs(parent, &sfs) < 0) + log_debug_errno(r, "Failed to statfs() %s, ignoring: %m", parent); + else + disk_ceiling = stat_used + sfs.f_bsize * sfs.f_bavail; + + } else if (S_ISBLK(st.st_mode)) { + _cleanup_free_ char *szbuf = NULL; + char p[SYS_BLOCK_PATH_MAX("/size")]; + + /* Let's read the size off sysfs, so that we don't have to open the device */ + xsprintf_sys_block_path(p, "/size", st.st_rdev); + r = read_one_line_file(p, &szbuf); + if (r < 0) + log_debug_errno(r, "Failed to read %s, ignoring: %m", p); + else { + uint64_t sz; + + r = safe_atou64(szbuf, &sz); + if (r < 0) + log_debug_errno(r, "Failed to parse %s, ignoring: %s", p, szbuf); + else + disk_size = sz * 512; + } + } else + log_debug("Image path is not a block device or regular file, not able to acquire size."); + } + } + + if (!HOME_STATE_IS_ACTIVE(state)) + goto finish; + + hd = user_record_home_directory(h->record); + if (!hd) + goto finish; + + if (statfs(hd, &sfs) < 0) { + log_debug_errno(errno, "Failed to statfs() %s, ignoring: %m", hd); + goto finish; + } + + disk_free = sfs.f_bsize * sfs.f_bavail; + fs_size = sfs.f_bsize * sfs.f_blocks; + if (disk_size != UINT64_MAX && disk_size > fs_size) + header_size = disk_size - fs_size; + + /* We take a perspective from the user here (as opposed to from the host): the used disk space is the + * difference from the limit and what's free. This makes a difference if sparse mode is not used: in + * that case the image is pre-allocated and thus appears all used from the host PoV but is not used + * up at all yet from the user's PoV. + * + * That said, we use use the stat() reported loopback file size as upper boundary: our footprint can + * never be larger than what we take up on the lowest layers. */ + + if (disk_size != UINT64_MAX && disk_size > disk_free) { + disk_usage = disk_size - disk_free; + + if (stat_used != UINT64_MAX && disk_usage > stat_used) + disk_usage = stat_used; + } else + disk_usage = stat_used; + + /* If we have the magic, determine floor preferably by magic */ + disk_floor = minimal_size_by_fs_magic(sfs.f_type) + header_size; + +finish: + /* If we don't know the magic, go by file system name */ + if (disk_floor == UINT64_MAX) + disk_floor = minimal_size_by_fs_name(user_record_file_system_type(h->record)); + + *ret_disk_size = disk_size; + *ret_disk_usage = disk_usage; + *ret_disk_free = disk_free; + *ret_disk_ceiling = disk_ceiling; + *ret_disk_floor = disk_floor; + + return 0; +} + +static int home_get_disk_status_directory( + Home *h, + HomeState state, + uint64_t *ret_disk_size, + uint64_t *ret_disk_usage, + uint64_t *ret_disk_free, + uint64_t *ret_disk_ceiling, + uint64_t *ret_disk_floor) { + + uint64_t disk_size = UINT64_MAX, disk_usage = UINT64_MAX, disk_free = UINT64_MAX, + disk_ceiling = UINT64_MAX, disk_floor = UINT64_MAX; + _cleanup_free_ char *devnode = NULL; + struct statfs sfs; + struct dqblk req; + const char *path = NULL; + dev_t devno; + int r; + + assert(ret_disk_size); + assert(ret_disk_usage); + assert(ret_disk_free); + assert(ret_disk_ceiling); + assert(ret_disk_floor); + + if (HOME_STATE_IS_ACTIVE(state)) + path = user_record_home_directory(h->record); + + if (!path) { + if (state == HOME_ABSENT) + goto finish; + + path = user_record_image_path(h->record); + } + + if (!path) + goto finish; + + if (statfs(path, &sfs) < 0) + log_debug_errno(errno, "Failed to statfs() %s, ignoring: %m", path); + else { + disk_free = sfs.f_bsize * sfs.f_bavail; + disk_size = sfs.f_bsize * sfs.f_blocks; + + /* We don't initialize disk_usage from statfs() data here, since the device is likely not used + * by us alone, and disk_usage should only reflect our own use. */ + } + + if (IN_SET(h->record->storage, USER_CLASSIC, USER_DIRECTORY, USER_SUBVOLUME)) { + + r = btrfs_is_subvol(path); + if (r < 0) + log_debug_errno(r, "Failed to determine whether %s is a btrfs subvolume: %m", path); + else if (r > 0) { + BtrfsQuotaInfo qi; + + r = btrfs_subvol_get_subtree_quota(path, 0, &qi); + if (r < 0) + log_debug_errno(r, "Failed to query btrfs subtree quota, ignoring: %m"); + else { + disk_usage = qi.referenced; + + if (disk_free != UINT64_MAX) { + disk_ceiling = qi.referenced + disk_free; + + if (disk_size != UINT64_MAX && disk_ceiling > disk_size) + disk_ceiling = disk_size; + } + + if (qi.referenced_max != UINT64_MAX) { + if (disk_size != UINT64_MAX) + disk_size = MIN(qi.referenced_max, disk_size); + else + disk_size = qi.referenced_max; + } + + if (disk_size != UINT64_MAX) { + if (disk_size > disk_usage) + disk_free = disk_size - disk_usage; + else + disk_free = 0; + } + } + + goto finish; + } + } + + if (IN_SET(h->record->storage, USER_CLASSIC, USER_DIRECTORY, USER_FSCRYPT)) { + + r = get_block_device(path, &devno); + if (r < 0) { + log_debug_errno(r, "Failed to determine block device of %s, ignoring: %m", path); + goto finish; + } + if (devno == 0) { + log_debug("File system %s not backed by a block device, can't get quota, ignoring.", path); + goto finish; + } + r = device_path_make_major_minor(S_IFBLK, devno, &devnode); + if (r < 0) { + log_debug_errno(r, "Failed to derive block device path for file system %s, ignoring: %m", path); + goto finish; + } + + if (quotactl(QCMD(Q_GETQUOTA, USRQUOTA), devnode, h->uid, (caddr_t) &req) < 0) { + + if (ERRNO_IS_NOT_SUPPORTED(errno)) { + log_debug_errno(errno, "No UID quota support on %s.", path); + goto finish; + } + + if (errno != ESRCH) { + log_debug_errno(errno, "Failed to query disk quota for UID " UID_FMT ": %m", h->uid); + goto finish; + } + + disk_usage = 0; /* No record of this user? then nothing was used */ + } else { + if (FLAGS_SET(req.dqb_valid, QIF_SPACE) && disk_free != UINT64_MAX) { + disk_ceiling = req.dqb_curspace + disk_free; + + if (disk_size != UINT64_MAX && disk_ceiling > disk_size) + disk_ceiling = disk_size; + } + + if (FLAGS_SET(req.dqb_valid, QIF_BLIMITS)) { + uint64_t q; + + /* Take the minimum of the quota and the available disk space here */ + q = req.dqb_bhardlimit * QIF_DQBLKSIZE; + if (disk_size != UINT64_MAX) + disk_size = MIN(disk_size, q); + else + disk_size = q; + } + if (FLAGS_SET(req.dqb_valid, QIF_SPACE)) { + disk_usage = req.dqb_curspace; + + if (disk_size != UINT64_MAX) { + if (disk_size > disk_usage) + disk_free = disk_size - disk_usage; + else + disk_free = 0; + } + } + } + } + +finish: + *ret_disk_size = disk_size; + *ret_disk_usage = disk_usage; + *ret_disk_free = disk_free; + *ret_disk_ceiling = disk_ceiling; + *ret_disk_floor = disk_floor; + + return 0; +} + +int home_augment_status( + Home *h, + UserRecordLoadFlags flags, + UserRecord **ret) { + + uint64_t disk_size = UINT64_MAX, disk_usage = UINT64_MAX, disk_free = UINT64_MAX, disk_ceiling = UINT64_MAX, disk_floor = UINT64_MAX; + _cleanup_(json_variant_unrefp) JsonVariant *j = NULL, *v = NULL, *m = NULL, *status = NULL; + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + char ids[SD_ID128_STRING_MAX]; + HomeState state; + sd_id128_t id; + int r; + + assert(h); + assert(ret); + + /* We are supposed to add this, this can't be on hence. */ + assert(!FLAGS_SET(flags, USER_RECORD_STRIP_STATUS)); + + r = sd_id128_get_machine(&id); + if (r < 0) + return r; + + state = home_get_state(h); + + switch (h->record->storage) { + + case USER_LUKS: + r = home_get_disk_status_luks(h, state, &disk_size, &disk_usage, &disk_free, &disk_ceiling, &disk_floor); + if (r < 0) + return r; + + break; + + case USER_CLASSIC: + case USER_DIRECTORY: + case USER_SUBVOLUME: + case USER_FSCRYPT: + case USER_CIFS: + r = home_get_disk_status_directory(h, state, &disk_size, &disk_usage, &disk_free, &disk_ceiling, &disk_floor); + if (r < 0) + return r; + + break; + + default: + ; /* unset */ + } + + if (disk_floor == UINT64_MAX || (disk_usage != UINT64_MAX && disk_floor < disk_usage)) + disk_floor = disk_usage; + if (disk_floor == UINT64_MAX || disk_floor < USER_DISK_SIZE_MIN) + disk_floor = USER_DISK_SIZE_MIN; + if (disk_ceiling == UINT64_MAX || disk_ceiling > USER_DISK_SIZE_MAX) + disk_ceiling = USER_DISK_SIZE_MAX; + + r = json_build(&status, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("state", JSON_BUILD_STRING(home_state_to_string(state))), + JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.Home")), + JSON_BUILD_PAIR_CONDITION(disk_size != UINT64_MAX, "diskSize", JSON_BUILD_UNSIGNED(disk_size)), + JSON_BUILD_PAIR_CONDITION(disk_usage != UINT64_MAX, "diskUsage", JSON_BUILD_UNSIGNED(disk_usage)), + JSON_BUILD_PAIR_CONDITION(disk_free != UINT64_MAX, "diskFree", JSON_BUILD_UNSIGNED(disk_free)), + JSON_BUILD_PAIR_CONDITION(disk_ceiling != UINT64_MAX, "diskCeiling", JSON_BUILD_UNSIGNED(disk_ceiling)), + JSON_BUILD_PAIR_CONDITION(disk_floor != UINT64_MAX, "diskFloor", JSON_BUILD_UNSIGNED(disk_floor)), + JSON_BUILD_PAIR_CONDITION(h->signed_locally >= 0, "signedLocally", JSON_BUILD_BOOLEAN(h->signed_locally)) + )); + if (r < 0) + return r; + + j = json_variant_ref(h->record->json); + v = json_variant_ref(json_variant_by_key(j, "status")); + m = json_variant_ref(json_variant_by_key(v, sd_id128_to_string(id, ids))); + + r = json_variant_filter(&m, STRV_MAKE("diskSize", "diskUsage", "diskFree", "diskCeiling", "diskFloor", "signedLocally")); + if (r < 0) + return r; + + r = json_variant_merge(&m, status); + if (r < 0) + return r; + + r = json_variant_set_field(&v, ids, m); + if (r < 0) + return r; + + r = json_variant_set_field(&j, "status", v); + if (r < 0) + return r; + + ur = user_record_new(); + if (!ur) + return -ENOMEM; + + r = user_record_load(ur, j, flags); + if (r < 0) + return r; + + ur->incomplete = + FLAGS_SET(h->record->mask, USER_RECORD_PRIVILEGED) && + !FLAGS_SET(ur->mask, USER_RECORD_PRIVILEGED); + + *ret = TAKE_PTR(ur); + return 0; +} + +static int on_home_ref_eof(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + _cleanup_(operation_unrefp) Operation *o = NULL; + Home *h = userdata; + + assert(s); + assert(h); + + if (h->ref_event_source_please_suspend == s) + h->ref_event_source_please_suspend = sd_event_source_disable_unref(h->ref_event_source_please_suspend); + + if (h->ref_event_source_dont_suspend == s) + h->ref_event_source_dont_suspend = sd_event_source_disable_unref(h->ref_event_source_dont_suspend); + + if (h->ref_event_source_dont_suspend || h->ref_event_source_please_suspend) + return 0; + + log_info("Got notification that all sessions of user %s ended, deactivating automatically.", h->user_name); + + o = operation_new(OPERATION_PIPE_EOF, NULL); + if (!o) { + log_oom(); + return 0; + } + + home_schedule_operation(h, o, NULL); + return 0; +} + +int home_create_fifo(Home *h, bool please_suspend) { + _cleanup_close_ int ret_fd = -1; + sd_event_source **ss; + const char *fn, *suffix; + int r; + + assert(h); + + if (please_suspend) { + suffix = ".please-suspend"; + ss = &h->ref_event_source_please_suspend; + } else { + suffix = ".dont-suspend"; + ss = &h->ref_event_source_dont_suspend; + } + + fn = strjoina("/run/systemd/home/", h->user_name, suffix); + + if (!*ss) { + _cleanup_close_ int ref_fd = -1; + + (void) mkdir("/run/systemd/home/", 0755); + if (mkfifo(fn, 0600) < 0 && errno != EEXIST) + return log_error_errno(errno, "Failed to create FIFO %s: %m", fn); + + ref_fd = open(fn, O_RDONLY|O_CLOEXEC|O_NONBLOCK); + if (ref_fd < 0) + return log_error_errno(errno, "Failed to open FIFO %s for reading: %m", fn); + + r = sd_event_add_io(h->manager->event, ss, ref_fd, 0, on_home_ref_eof, h); + if (r < 0) + return log_error_errno(r, "Failed to allocate reference FIFO event source: %m"); + + (void) sd_event_source_set_description(*ss, "acquire-ref"); + + r = sd_event_source_set_priority(*ss, SD_EVENT_PRIORITY_IDLE-1); + if (r < 0) + return r; + + r = sd_event_source_set_io_fd_own(*ss, true); + if (r < 0) + return log_error_errno(r, "Failed to pass ownership of FIFO event fd to event source: %m"); + + TAKE_FD(ref_fd); + } + + ret_fd = open(fn, O_WRONLY|O_CLOEXEC|O_NONBLOCK); + if (ret_fd < 0) + return log_error_errno(errno, "Failed to open FIFO %s for writing: %m", fn); + + return TAKE_FD(ret_fd); +} + +static int home_dispatch_acquire(Home *h, Operation *o) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int (*call)(Home *h, UserRecord *secret, HomeState for_state, sd_bus_error *error) = NULL; + HomeState for_state; + int r; + + assert(h); + assert(o); + assert(o->type == OPERATION_ACQUIRE); + + switch (home_get_state(h)) { + + case HOME_UNFIXATED: + for_state = HOME_FIXATING_FOR_ACQUIRE; + call = home_fixate_internal; + break; + + case HOME_ABSENT: + r = sd_bus_error_setf(&error, BUS_ERROR_HOME_ABSENT, "Home %s is currently missing or not plugged in.", h->user_name); + break; + + case HOME_INACTIVE: + for_state = HOME_ACTIVATING_FOR_ACQUIRE; + call = home_activate_internal; + break; + + case HOME_ACTIVE: + for_state = HOME_AUTHENTICATING_FOR_ACQUIRE; + call = home_authenticate_internal; + break; + + case HOME_LOCKED: + for_state = HOME_UNLOCKING_FOR_ACQUIRE; + call = home_unlock_internal; + break; + + default: + /* All other cases means we are currently executing an operation, which means the job remains + * pending. */ + return 0; + } + + assert(!h->current_operation); + + if (call) { + r = home_ratelimit(h, &error); + if (r >= 0) + r = call(h, o->secret, for_state, &error); + } + + if (r != 0) /* failure or completed */ + operation_result(o, r, &error); + else /* ongoing */ + h->current_operation = operation_ref(o); + + return 1; +} + +static int home_dispatch_release(Home *h, Operation *o) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(o); + assert(o->type == OPERATION_RELEASE); + + if (h->ref_event_source_dont_suspend || h->ref_event_source_please_suspend) + /* If there's now a reference again, then let's abort the release attempt */ + r = sd_bus_error_setf(&error, BUS_ERROR_HOME_BUSY, "Home %s is currently referenced.", h->user_name); + else { + switch (home_get_state(h)) { + + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + r = 0; /* done */ + break; + + case HOME_LOCKED: + r = sd_bus_error_setf(&error, BUS_ERROR_HOME_LOCKED, "Home %s is currently locked.", h->user_name); + break; + + case HOME_ACTIVE: + r = home_deactivate_internal(h, false, &error); + break; + + default: + /* All other cases means we are currently executing an operation, which means the job remains + * pending. */ + return 0; + } + } + + assert(!h->current_operation); + + if (r <= 0) /* failure or completed */ + operation_result(o, r, &error); + else /* ongoing */ + h->current_operation = operation_ref(o); + + return 1; +} + +static int home_dispatch_lock_all(Home *h, Operation *o) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(o); + assert(o->type == OPERATION_LOCK_ALL); + + switch (home_get_state(h)) { + + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + log_info("Home %s is not active, no locking necessary.", h->user_name); + r = 0; /* done */ + break; + + case HOME_LOCKED: + log_info("Home %s is already locked.", h->user_name); + r = 0; /* done */ + break; + + case HOME_ACTIVE: + log_info("Locking home %s.", h->user_name); + r = home_lock(h, &error); + break; + + default: + /* All other cases means we are currently executing an operation, which means the job remains + * pending. */ + return 0; + } + + assert(!h->current_operation); + + if (r != 0) /* failure or completed */ + operation_result(o, r, &error); + else /* ongoing */ + h->current_operation = operation_ref(o); + + return 1; +} + +static int home_dispatch_pipe_eof(Home *h, Operation *o) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(o); + assert(o->type == OPERATION_PIPE_EOF); + + if (h->ref_event_source_please_suspend || h->ref_event_source_dont_suspend) + return 1; /* Hmm, there's a reference again, let's cancel this */ + + switch (home_get_state(h)) { + + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + log_info("Home %s already deactivated, no automatic deactivation needed.", h->user_name); + break; + + case HOME_DEACTIVATING: + log_info("Home %s is already being deactivated, automatic deactivated unnecessary.", h->user_name); + break; + + case HOME_ACTIVE: + r = home_deactivate_internal(h, false, &error); + if (r < 0) + log_warning_errno(r, "Failed to deactivate %s, ignoring: %s", h->user_name, bus_error_message(&error, r)); + break; + + case HOME_LOCKED: + default: + /* If the device is locked or any operation is being executed, let's leave this pending */ + return 0; + } + + /* Note that we don't call operation_fail() or operation_success() here, because this kind of + * operation has no message associated with it, and thus there's no need to propagate success. */ + + assert(!o->message); + return 1; +} + +static int home_dispatch_deactivate_force(Home *h, Operation *o) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + int r; + + assert(h); + assert(o); + assert(o->type == OPERATION_DEACTIVATE_FORCE); + + switch (home_get_state(h)) { + + case HOME_UNFIXATED: + case HOME_ABSENT: + case HOME_INACTIVE: + log_debug("Home %s already deactivated, no forced deactivation due to unplug needed.", h->user_name); + break; + + case HOME_DEACTIVATING: + log_debug("Home %s is already being deactivated, forced deactivation due to unplug unnecessary.", h->user_name); + break; + + case HOME_ACTIVE: + case HOME_LOCKED: + r = home_deactivate_internal(h, true, &error); + if (r < 0) + log_warning_errno(r, "Failed to forcibly deactivate %s, ignoring: %s", h->user_name, bus_error_message(&error, r)); + break; + + default: + /* If any operation is being executed, let's leave this pending */ + return 0; + } + + /* Note that we don't call operation_fail() or operation_success() here, because this kind of + * operation has no message associated with it, and thus there's no need to propagate success. */ + + assert(!o->message); + return 1; +} + +static int on_pending(sd_event_source *s, void *userdata) { + Home *h = userdata; + Operation *o; + int r; + + assert(s); + assert(h); + + o = ordered_set_first(h->pending_operations); + if (o) { + static int (* const operation_table[_OPERATION_MAX])(Home *h, Operation *o) = { + [OPERATION_ACQUIRE] = home_dispatch_acquire, + [OPERATION_RELEASE] = home_dispatch_release, + [OPERATION_LOCK_ALL] = home_dispatch_lock_all, + [OPERATION_PIPE_EOF] = home_dispatch_pipe_eof, + [OPERATION_DEACTIVATE_FORCE] = home_dispatch_deactivate_force, + }; + + assert(operation_table[o->type]); + r = operation_table[o->type](h, o); + if (r != 0) { + /* The operation completed, let's remove it from the pending list, and exit while + * leaving the event source enabled as it is. */ + assert_se(ordered_set_remove(h->pending_operations, o) == o); + operation_unref(o); + return 0; + } + } + + /* Nothing to do anymore, let's turn off this event source */ + r = sd_event_source_set_enabled(s, SD_EVENT_OFF); + if (r < 0) + return log_error_errno(r, "Failed to disable event source: %m"); + + return 0; +} + +int home_schedule_operation(Home *h, Operation *o, sd_bus_error *error) { + int r; + + assert(h); + + if (o) { + if (ordered_set_size(h->pending_operations) >= PENDING_OPERATIONS_MAX) + return sd_bus_error_setf(error, BUS_ERROR_TOO_MANY_OPERATIONS, "Too many client operations requested"); + + r = ordered_set_ensure_allocated(&h->pending_operations, &operation_hash_ops); + if (r < 0) + return r; + + r = ordered_set_put(h->pending_operations, o); + if (r < 0) + return r; + + operation_ref(o); + } + + if (!h->pending_event_source) { + r = sd_event_add_defer(h->manager->event, &h->pending_event_source, on_pending, h); + if (r < 0) + return log_error_errno(r, "Failed to allocate pending defer event source: %m"); + + (void) sd_event_source_set_description(h->pending_event_source, "pending"); + + r = sd_event_source_set_priority(h->pending_event_source, SD_EVENT_PRIORITY_IDLE); + if (r < 0) + return r; + } + + r = sd_event_source_set_enabled(h->pending_event_source, SD_EVENT_ON); + if (r < 0) + return log_error_errno(r, "Failed to trigger pending event source: %m"); + + return 0; +} + +static int home_get_image_path_seat(Home *h, char **ret) { + _cleanup_(sd_device_unrefp) sd_device *d = NULL; + _cleanup_free_ char *c = NULL; + const char *ip, *seat; + struct stat st; + int r; + + assert(h); + + if (user_record_storage(h->record) != USER_LUKS) + return -ENXIO; + + ip = user_record_image_path(h->record); + if (!ip) + return -ENXIO; + + if (!path_startswith(ip, "/dev/")) + return -ENXIO; + + if (stat(ip, &st) < 0) + return -errno; + + if (!S_ISBLK(st.st_mode)) + return -ENOTBLK; + + r = sd_device_new_from_devnum(&d, 'b', st.st_rdev); + if (r < 0) + return r; + + r = sd_device_get_property_value(d, "ID_SEAT", &seat); + if (r == -ENOENT) /* no property means seat0 */ + seat = "seat0"; + else if (r < 0) + return r; + + c = strdup(seat); + if (!c) + return -ENOMEM; + + *ret = TAKE_PTR(c); + return 0; +} + +int home_auto_login(Home *h, char ***ret_seats) { + _cleanup_free_ char *seat = NULL, *seat2 = NULL; + + assert(h); + assert(ret_seats); + + (void) home_get_image_path_seat(h, &seat); + + if (h->record->auto_login > 0 && !streq_ptr(seat, "seat0")) { + /* For now, when the auto-login boolean is set for a user, let's make it mean + * "seat0". Eventually we can extend the concept and allow configuration of any kind of seat, + * but let's keep simple initially, most likely the feature is interesting on single-user + * systems anyway, only. + * + * We filter out users marked for auto-login in we know for sure their home directory is + * absent. */ + + if (user_record_test_image_path(h->record) != USER_TEST_ABSENT) { + seat2 = strdup("seat0"); + if (!seat2) + return -ENOMEM; + } + } + + if (seat || seat2) { + _cleanup_strv_free_ char **list = NULL; + size_t i = 0; + + list = new(char*, 3); + if (!list) + return -ENOMEM; + + if (seat) + list[i++] = TAKE_PTR(seat); + if (seat2) + list[i++] = TAKE_PTR(seat2); + + list[i] = NULL; + *ret_seats = TAKE_PTR(list); + return 1; + } + + *ret_seats = NULL; + return 0; +} + +int home_set_current_message(Home *h, sd_bus_message *m) { + assert(h); + + if (!m) + return 0; + + if (h->current_operation) + return -EBUSY; + + h->current_operation = operation_new(OPERATION_IMMEDIATE, m); + if (!h->current_operation) + return -ENOMEM; + + return 1; +} + +static const char* const home_state_table[_HOME_STATE_MAX] = { + [HOME_UNFIXATED] = "unfixated", + [HOME_ABSENT] = "absent", + [HOME_INACTIVE] = "inactive", + [HOME_FIXATING] = "fixating", + [HOME_FIXATING_FOR_ACTIVATION] = "fixating-for-activation", + [HOME_FIXATING_FOR_ACQUIRE] = "fixating-for-acquire", + [HOME_ACTIVATING] = "activating", + [HOME_ACTIVATING_FOR_ACQUIRE] = "activating-for-acquire", + [HOME_DEACTIVATING] = "deactivating", + [HOME_ACTIVE] = "active", + [HOME_LOCKING] = "locking", + [HOME_LOCKED] = "locked", + [HOME_UNLOCKING] = "unlocking", + [HOME_UNLOCKING_FOR_ACQUIRE] = "unlocking-for-acquire", + [HOME_CREATING] = "creating", + [HOME_REMOVING] = "removing", + [HOME_UPDATING] = "updating", + [HOME_UPDATING_WHILE_ACTIVE] = "updating-while-active", + [HOME_RESIZING] = "resizing", + [HOME_RESIZING_WHILE_ACTIVE] = "resizing-while-active", + [HOME_PASSWD] = "passwd", + [HOME_PASSWD_WHILE_ACTIVE] = "passwd-while-active", + [HOME_AUTHENTICATING] = "authenticating", + [HOME_AUTHENTICATING_WHILE_ACTIVE] = "authenticating-while-active", + [HOME_AUTHENTICATING_FOR_ACQUIRE] = "authenticating-for-acquire", +}; + +DEFINE_STRING_TABLE_LOOKUP(home_state, HomeState); diff --git a/src/home/homed-home.h b/src/home/homed-home.h new file mode 100644 index 0000000000000..c75b06722c625 --- /dev/null +++ b/src/home/homed-home.h @@ -0,0 +1,168 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +typedef struct Home Home; + +#include "homed-manager.h" +#include "homed-operation.h" +#include "list.h" +#include "ordered-set.h" +#include "user-record.h" + +typedef enum HomeState { + HOME_UNFIXATED, /* home exists, but local record does not */ + HOME_ABSENT, /* local record exists, but home does not */ + HOME_INACTIVE, /* record and home exist, but is not logged in */ + HOME_FIXATING, /* generating local record from home */ + HOME_FIXATING_FOR_ACTIVATION, /* fixating in order to activate soon */ + HOME_FIXATING_FOR_ACQUIRE, /* fixating because Acquire() was called */ + HOME_ACTIVATING, + HOME_ACTIVATING_FOR_ACQUIRE, /* activating because Acquire() was called */ + HOME_DEACTIVATING, + HOME_ACTIVE, /* logged in right now */ + HOME_LOCKING, + HOME_LOCKED, + HOME_UNLOCKING, + HOME_UNLOCKING_FOR_ACQUIRE, /* unlocking because Acquire() was called */ + HOME_CREATING, + HOME_REMOVING, + HOME_UPDATING, + HOME_UPDATING_WHILE_ACTIVE, + HOME_RESIZING, + HOME_RESIZING_WHILE_ACTIVE, + HOME_PASSWD, + HOME_PASSWD_WHILE_ACTIVE, + HOME_AUTHENTICATING, + HOME_AUTHENTICATING_WHILE_ACTIVE, + HOME_AUTHENTICATING_FOR_ACQUIRE, /* authenticating because Acquire() was called */ + _HOME_STATE_MAX, + _HOME_STATE_INVALID = -1 +} HomeState; + +static inline bool HOME_STATE_IS_ACTIVE(HomeState state) { + return IN_SET(state, + HOME_ACTIVE, + HOME_UPDATING_WHILE_ACTIVE, + HOME_RESIZING_WHILE_ACTIVE, + HOME_PASSWD_WHILE_ACTIVE, + HOME_AUTHENTICATING_WHILE_ACTIVE, + HOME_AUTHENTICATING_FOR_ACQUIRE); +} + +static inline bool HOME_STATE_IS_EXECUTING_OPERATION(HomeState state) { + return IN_SET(state, + HOME_FIXATING, + HOME_FIXATING_FOR_ACTIVATION, + HOME_FIXATING_FOR_ACQUIRE, + HOME_ACTIVATING, + HOME_ACTIVATING_FOR_ACQUIRE, + HOME_DEACTIVATING, + HOME_LOCKING, + HOME_UNLOCKING, + HOME_UNLOCKING_FOR_ACQUIRE, + HOME_CREATING, + HOME_REMOVING, + HOME_UPDATING, + HOME_UPDATING_WHILE_ACTIVE, + HOME_RESIZING, + HOME_RESIZING_WHILE_ACTIVE, + HOME_PASSWD, + HOME_PASSWD_WHILE_ACTIVE, + HOME_AUTHENTICATING, + HOME_AUTHENTICATING_WHILE_ACTIVE, + HOME_AUTHENTICATING_FOR_ACQUIRE); +} + +struct Home { + Manager *manager; + char *user_name; + uid_t uid; + + char *sysfs; /* When found via plugged in device, the sysfs path to it */ + + /* Note that the 'state' field is only set to a state while we are doing something (i.e. activating, + * deactivating, creating, removing, and such), or when the home is an "unfixated" one. When we are + * done with an operation we invalidate the state. This is hint for home_get_state() to check the + * state on request as needed from the mount table and similar.*/ + HomeState state; + int signed_locally; /* signed only by us */ + + UserRecord *record; + + pid_t worker_pid; + int worker_stdout_fd; + sd_event_source *worker_event_source; + int worker_error_code; + + /* The message we are currently processing, and thus need to reply to on completion */ + Operation *current_operation; + + /* Stores the raw, plaintext passwords, but only for short periods of time */ + UserRecord *secret; + + /* When we create a home and that fails, we should possibly unregister the record altogether + * again, which is remembered in this boolean. */ + bool unregister_on_failure; + + /* The reading side of a FIFO stored in /run/systemd/home/, the writing side being used for reference + * counting. The references dropped to zero as soon as we see EOF. This concept exists twice: once + * for clients that are fine if we suspend the home directory on system suspend, and once for cliets + * that are not ok with that. This allows us to determine for each home whether there are any clients + * that support unsuspend. */ + sd_event_source *ref_event_source_please_suspend; + sd_event_source *ref_event_source_dont_suspend; + + /* Any pending operations we still need to execute. These are for operations we want to queue if we + * can't execute them right-away. */ + OrderedSet *pending_operations; + + /* A defer event source that processes pending acquire/release/eof events. We have a common + * dispatcher that processes all three kinds of events. */ + sd_event_source *pending_event_source; + + /* Did we send out a D-Bus notification about this entry? */ + bool announced; + + /* Used to coalesce bus PropertiesChanged events */ + sd_event_source *deferred_change_event_source; +}; + +int home_new(Manager *m, UserRecord *hr, const char *sysfs, Home **ret); +Home *home_free(Home *h); + +DEFINE_TRIVIAL_CLEANUP_FUNC(Home*, home_free); + +int home_set_record(Home *h, UserRecord *hr); +int home_save_record(Home *h); +int home_unlink_record(Home *h); + +int home_fixate(Home *h, UserRecord *secret, sd_bus_error *error); +int home_activate(Home *h, UserRecord *secret, sd_bus_error *error); +int home_authenticate(Home *h, UserRecord *secret, sd_bus_error *error); +int home_deactivate(Home *h, bool force, sd_bus_error *error); +int home_create(Home *h, UserRecord *secret, sd_bus_error *error); +int home_remove(Home *h, sd_bus_error *error); +int home_update(Home *h, UserRecord *new_record, sd_bus_error *error); +int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, sd_bus_error *error); +int home_passwd(Home *h, UserRecord *new_secret, UserRecord *old_secret, sd_bus_error *error); +int home_unregister(Home *h, sd_bus_error *error); +int home_lock(Home *h, sd_bus_error *error); +int home_unlock(Home *h, UserRecord *secret, sd_bus_error *error); + +HomeState home_get_state(Home *h); + +void home_process_notify(Home *h, char **l); + +int home_killall(Home *h); + +int home_augment_status(Home *h, UserRecordLoadFlags flags, UserRecord **ret); + +int home_create_fifo(Home *h, bool please_suspend); +int home_schedule_operation(Home *h, Operation *o, sd_bus_error *error); + +int home_auto_login(Home *h, char ***ret_seats); + +int home_set_current_message(Home *h, sd_bus_message *m); + +const char *home_state_to_string(HomeState state); +HomeState home_state_from_string(const char *s); diff --git a/src/home/homed-manager-bus.c b/src/home/homed-manager-bus.c new file mode 100644 index 0000000000000..38fa6f230573a --- /dev/null +++ b/src/home/homed-manager-bus.c @@ -0,0 +1,690 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "alloc-util.h" +#include "bus-common-errors.h" +#include "bus-util.h" +#include "format-util.h" +#include "homed-bus.h" +#include "homed-home-bus.h" +#include "homed-manager-bus.h" +#include "homed-manager.h" +#include "strv.h" +#include "user-record-sign.h" +#include "user-record-util.h" +#include "user-util.h" + +static int property_get_auto_login( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Manager *m = userdata; + Iterator i; + Home *h; + int r; + + assert(bus); + assert(reply); + assert(m); + + r = sd_bus_message_open_container(reply, 'a', "(sso)"); + if (r < 0) + return r; + + HASHMAP_FOREACH(h, m->homes_by_name, i) { + _cleanup_(strv_freep) char **seats = NULL; + _cleanup_free_ char *home_path = NULL; + char **s; + + r = home_auto_login(h, &seats); + if (r < 0) { + log_debug_errno(r, "Failed to determine whether home '%s' is candidate for auto-login, ignoring: %m", h->user_name); + continue; + } + if (!r) + continue; + + r = bus_home_path(h, &home_path); + if (r < 0) + return log_error_errno(r, "Failed to generate home bus path: %m"); + + STRV_FOREACH(s, seats) { + r = sd_bus_message_append(reply, "(sso)", h->user_name, *s, home_path); + if (r < 0) + return r; + } + } + + return sd_bus_message_close_container(reply); +} + +static int method_get_home_by_name( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_free_ char *path = NULL; + const char *user_name; + Manager *m = userdata; + Home *h; + int r; + + assert(message); + assert(m); + + r = sd_bus_message_read(message, "s", &user_name); + if (r < 0) + return r; + if (!valid_user_group_name(user_name)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name); + + h = hashmap_get(m->homes_by_name, user_name); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name); + + r = bus_home_path(h, &path); + if (r < 0) + return r; + + return sd_bus_reply_method_return( + message, "usussso", + (uint32_t) h->uid, + home_state_to_string(home_get_state(h)), + h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID, + h->record ? user_record_real_name(h->record) : NULL, + h->record ? user_record_home_directory(h->record) : NULL, + h->record ? user_record_shell(h->record) : NULL, + path); +} + +static int method_get_home_by_uid( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_free_ char *path = NULL; + Manager *m = userdata; + uint32_t uid; + int r; + Home *h; + + assert(message); + assert(m); + + r = sd_bus_message_read(message, "u", &uid); + if (r < 0) + return r; + if (!uid_is_valid(uid)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "UID " UID_FMT " is not valid", uid); + + h = hashmap_get(m->homes_by_uid, UID_TO_PTR(uid)); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for UID " UID_FMT " known", uid); + + /* Note that we don't use bus_home_path() here, but build the path manually, since if we are queried + * for a UID we should also generate the bus path with a UID, and bus_home_path() uses our more + * typical bus path by name. */ + if (asprintf(&path, "/org/freedesktop/home1/home/" UID_FMT, h->uid) < 0) + return -ENOMEM; + + return sd_bus_reply_method_return( + message, "ssussso", + h->user_name, + home_state_to_string(home_get_state(h)), + h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID, + h->record ? user_record_real_name(h->record) : NULL, + h->record ? user_record_home_directory(h->record) : NULL, + h->record ? user_record_shell(h->record) : NULL, + path); +} + +static int method_list_homes( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + Manager *m = userdata; + Iterator i; + Home *h; + int r; + + assert(message); + assert(m); + + r = sd_bus_message_new_method_return(message, &reply); + if (r < 0) + return r; + + r = sd_bus_message_open_container(reply, 'a', "(susussso)"); + if (r < 0) + return r; + + HASHMAP_FOREACH(h, m->homes_by_uid, i) { + _cleanup_free_ char *path = NULL; + + r = bus_home_path(h, &path); + if (r < 0) + return r; + + r = sd_bus_message_append( + reply, "(susussso)", + h->user_name, + (uint32_t) h->uid, + home_state_to_string(home_get_state(h)), + h->record ? (uint32_t) user_record_gid(h->record) : GID_INVALID, + h->record ? user_record_real_name(h->record) : NULL, + h->record ? user_record_home_directory(h->record) : NULL, + h->record ? user_record_shell(h->record) : NULL, + path); + if (r < 0) + return r; + } + + r = sd_bus_message_close_container(reply); + if (r < 0) + return r; + + return sd_bus_send(NULL, reply, NULL); +} + +static int method_get_user_record_by_name( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_free_ char *json = NULL, *path = NULL; + Manager *m = userdata; + const char *user_name; + bool incomplete; + Home *h; + int r; + + assert(message); + assert(m); + + r = sd_bus_message_read(message, "s", &user_name); + if (r < 0) + return r; + if (!valid_user_group_name(user_name)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name); + + h = hashmap_get(m->homes_by_name, user_name); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name); + + r = bus_home_get_record_json(h, message, &json, &incomplete); + if (r < 0) + return r; + + r = bus_home_path(h, &path); + if (r < 0) + return r; + + return sd_bus_reply_method_return( + message, "sbo", + json, + incomplete, + path); +} + +static int method_get_user_record_by_uid( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_free_ char *json = NULL, *path = NULL; + Manager *m = userdata; + bool incomplete; + uint32_t uid; + Home *h; + int r; + + assert(message); + assert(m); + + r = sd_bus_message_read(message, "u", &uid); + if (r < 0) + return r; + if (!uid_is_valid(uid)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "UID " UID_FMT " is not valid", uid); + + h = hashmap_get(m->homes_by_uid, UID_TO_PTR(uid)); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for UID " UID_FMT " known", uid); + + r = bus_home_get_record_json(h, message, &json, &incomplete); + if (r < 0) + return r; + + if (asprintf(&path, "/org/freedesktop/home1/home/" UID_FMT, h->uid) < 0) + return -ENOMEM; + + return sd_bus_reply_method_return( + message, "sbo", + json, + incomplete, + path); +} + +static int generic_home_method( + Manager *m, + sd_bus_message *message, + sd_bus_message_handler_t handler, + sd_bus_error *error) { + + const char *user_name; + Home *h; + int r; + + r = sd_bus_message_read(message, "s", &user_name); + if (r < 0) + return r; + + if (!valid_user_group_name(user_name)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User name %s is not valid", user_name); + + h = hashmap_get(m->homes_by_name, user_name); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", user_name); + + return handler(message, h, error); +} + +static int method_activate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_activate, error); +} + +static int method_deactivate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_deactivate, error); +} + +static int validate_and_allocate_home(Manager *m, UserRecord *hr, Home **ret, sd_bus_error *error) { + _cleanup_(user_record_unrefp) UserRecord *signed_hr = NULL; + struct passwd *pw; + struct group *gr; + bool signed_locally; + Home *other; + int r; + + assert(m); + assert(hr); + assert(ret); + + r = user_record_is_supported(hr, error); + if (r < 0) + return r; + + other = hashmap_get(m->homes_by_name, hr->user_name); + if (other) + return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s exists already, refusing.", hr->user_name); + + pw = getpwnam(hr->user_name); + if (pw) + return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s exists in the NSS user database, refusing.", hr->user_name); + + gr = getgrnam(hr->user_name); + if (gr) + return sd_bus_error_setf(error, BUS_ERROR_USER_NAME_EXISTS, "Specified user name %s conflicts with an NSS group by the same name, refusing.", hr->user_name); + + r = manager_verify_user_record(m, hr); + switch (r) { + + case USER_RECORD_UNSIGNED: + /* If the record is unsigned, then let's sign it with our own key */ + r = manager_sign_user_record(m, hr, &signed_hr, error); + if (r < 0) + return r; + + hr = signed_hr; + _fallthrough_; + + case USER_RECORD_SIGNED_EXCLUSIVE: + signed_locally = true; + break; + + case USER_RECORD_SIGNED: + case USER_RECORD_FOREIGN: + signed_locally = false; + break; + + case -ENOKEY: + return sd_bus_error_setf(error, BUS_ERROR_BAD_SIGNATURE, "Specified user record for %s is signed by a key we don't recognize, refusing.", hr->user_name); + + default: + return sd_bus_error_set_errnof(error, r, "Failed to validate signature for '%s': %m", hr->user_name); + } + + if (uid_is_valid(hr->uid)) { + other = hashmap_get(m->homes_by_uid, UID_TO_PTR(hr->uid)); + if (other) + return sd_bus_error_setf(error, BUS_ERROR_UID_IN_USE, "Specified UID " UID_FMT " already in use by home %s, refusing.", hr->uid, other->user_name); + + pw = getpwuid(hr->uid); + if (pw) + return sd_bus_error_setf(error, BUS_ERROR_UID_IN_USE, "Specified UID " UID_FMT " already in use by NSS user %s, refusing.", hr->uid, pw->pw_name); + + gr = getgrgid(hr->uid); + if (gr) + return sd_bus_error_setf(error, BUS_ERROR_UID_IN_USE, "Specified UID " UID_FMT " already in use as GID by NSS group %s, refusing.", hr->uid, gr->gr_name); + } else { + r = manager_augment_record_with_uid(m, hr); + if (r < 0) + return sd_bus_error_set_errnof(error, r, "Failed to acquire UID for '%s': %m", hr->user_name); + } + + r = home_new(m, hr, NULL, ret); + if (r < 0) + return r; + + (*ret)->signed_locally = signed_locally; + return r; +} + +static int method_register_home( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + Manager *m = userdata; + Home *h; + int r; + + assert(message); + assert(m); + + r = bus_message_read_home_record(message, USER_RECORD_LOAD_EMBEDDED, &hr, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.create-home", + NULL, + true, + UID_INVALID, + &m->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = validate_and_allocate_home(m, hr, &h, error); + if (r < 0) + return r; + + r = home_save_record(h); + if (r < 0) { + home_free(h); + return r; + } + + return sd_bus_reply_method_return(message, NULL); +} + +static int method_unregister_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_unregister, error); +} + +static int method_create_home( + sd_bus_message *message, + void *userdata, + sd_bus_error *error) { + + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + Manager *m = userdata; + Home *h; + int r; + + assert(message); + assert(m); + + r = bus_message_read_home_record(message, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_REQUIRE_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE, &hr, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async( + message, + CAP_SYS_ADMIN, + "org.freedesktop.home1.create-home", + NULL, + true, + UID_INVALID, + &m->polkit_registry, + error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Will call us back */ + + r = validate_and_allocate_home(m, hr, &h, error); + if (r < 0) + return r; + + r = home_create(h, hr, error); + if (r < 0) + goto fail; + + assert(r == 0); + h->unregister_on_failure = true; + assert(!h->current_operation); + + r = home_set_current_message(h, message); + if (r < 0) + return r; + + return 1; + +fail: + (void) home_unlink_record(h); + h = home_free(h); + return r; +} + +static int method_realize_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_realize, error); +} + +static int method_remove_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_remove, error); +} + +static int method_fixate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_fixate, error); +} + +static int method_authenticate_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_authenticate, error); +} + +static int method_update_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + Manager *m = userdata; + Home *h; + int r; + + assert(message); + assert(m); + + r = bus_message_read_home_record(message, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_REQUIRE_SECRET|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_SIGNATURE, &hr, error); + if (r < 0) + return r; + + assert(hr->user_name); + + h = hashmap_get(m->homes_by_name, hr->user_name); + if (!h) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", hr->user_name); + + return bus_home_method_update_record(h, message, hr, error); +} + +static int method_resize_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_resize, error); +} + +static int method_change_password_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_change_password, error); +} + +static int method_lock_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_lock, error); +} + +static int method_unlock_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_unlock, error); +} + +static int method_acquire_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_acquire, error); +} + +static int method_ref_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_ref, error); +} + +static int method_release_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return generic_home_method(userdata, message, bus_home_method_release, error); +} + +static int method_lock_all_homes(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_(operation_unrefp) Operation *o = NULL; + bool waiting = false; + Manager *m = userdata; + Iterator i; + Home *h; + int r; + + assert(m); + + /* This is called from logind when we are preparing for system suspend. We enqueue a lock operation + * for every suitable home we have and only when all of them completed we send a reply indicating + * completion. */ + + HASHMAP_FOREACH(h, m->homes_by_name, i) { + + /* Automatically suspend all homes that have at least one client referencing it that asked + * for "please suspend", and no client that asked for "please do not suspend". */ + if (h->ref_event_source_dont_suspend || + !h->ref_event_source_please_suspend) + continue; + + if (!o) { + o = operation_new(OPERATION_LOCK_ALL, message); + if (!o) + return -ENOMEM; + } + + log_info("Automatically locking of home of user %s.", h->user_name); + + r = home_schedule_operation(h, o, error); + if (r < 0) + return r; + + waiting = true; + } + + if (waiting) /* At least one lock operation was enqeued, let's leave here without a reply: it will + * be sent as soon as the last of the lock operations completed. */ + return 1; + + return sd_bus_reply_method_return(message, NULL); +} + +const sd_bus_vtable manager_vtable[] = { + SD_BUS_VTABLE_START(0), + + SD_BUS_PROPERTY("AutoLogin", "a(sso)", property_get_auto_login, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + + SD_BUS_METHOD("GetHomeByName", "s", "usussso", method_get_home_by_name, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("GetHomeByUID", "u", "ssussso", method_get_home_by_uid, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("GetUserRecordByName", "s", "sbo", method_get_user_record_by_name, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("GetUserRecordByUID", "u", "sbo", method_get_user_record_by_uid, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("ListHomes", NULL, "a(susussso)", method_list_homes, SD_BUS_VTABLE_UNPRIVILEGED), + + /* The following methods directly execute an operation on a home, without ref-counting, queing or + * anything, and are accessible through homectl. */ + SD_BUS_METHOD("ActivateHome", "ss", NULL, method_activate_home, SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("DeactivateHome", "s", NULL, method_deactivate_home, 0), + SD_BUS_METHOD("RegisterHome", "s", NULL, method_register_home, SD_BUS_VTABLE_UNPRIVILEGED), /* Add JSON record to homed, but don't create actual $HOME */ + SD_BUS_METHOD("UnregisterHome", "s", NULL, method_unregister_home, SD_BUS_VTABLE_UNPRIVILEGED), /* Remove JSON record from homed, but don't remove actual $HOME */ + SD_BUS_METHOD("CreateHome", "s", NULL, method_create_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), /* Add JSON record, and create $HOME for it */ + SD_BUS_METHOD("RealizeHome", "ss", NULL, method_realize_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), /* Create $HOME for already registered JSON entry */ + SD_BUS_METHOD("RemoveHome", "s", NULL, method_remove_home, SD_BUS_VTABLE_UNPRIVILEGED), /* Remove JSON record and remove $HOME */ + SD_BUS_METHOD("FixateHome", "ss", NULL, method_fixate_home, SD_BUS_VTABLE_SENSITIVE), /* Investigate $HOME and propagate contained JSON record into our database */ + SD_BUS_METHOD("AuthenticateHome", "ss", NULL, method_authenticate_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), /* Just check credentials */ + SD_BUS_METHOD("UpdateHome", "s", NULL, method_update_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), /* Update JSON record of existing user */ + SD_BUS_METHOD("ResizeHome", "sts", NULL, method_resize_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("ChangePasswordHome", "sss", NULL, method_change_password_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("LockHome", "s", NULL, method_lock_home, 0), /* Prepare active home for system suspend: flush out passwords, suspend access */ + SD_BUS_METHOD("UnlockHome", "ss", NULL, method_unlock_home, SD_BUS_VTABLE_SENSITIVE), /* Make $HOME usable after system resume again */ + + /* The following methods implement ref-counted activation, and are what the PAM module calls (and + * what "homectl with" runs). In contrast to the methods above which fail if an operation is already + * being executed on a home directory, these ones will queue the request, and are thus more + * reliable. Moreover, they are a bit smarter: AcquireHome() will fixate, activate, unlock, or + * authenticate depending on the state of the home, so that the end result is always the same + * (i.e. the home directory is accessible), and we always validate the specified passwords. RefHome() + * will not authenticate, and thus only works if home is already active. */ + SD_BUS_METHOD("AcquireHome", "ssb", "h", method_acquire_home, SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD("RefHome", "sb", "h", method_ref_home, 0), + SD_BUS_METHOD("ReleaseHome", "s", NULL, method_release_home, 0), + + /* An operation that acts on all homes that allow it */ + SD_BUS_METHOD("LockAllHomes", NULL, NULL, method_lock_all_homes, 0), + + SD_BUS_VTABLE_END +}; + +static int on_deferred_auto_login(sd_event_source *s, void *userdata) { + Manager *m = userdata; + int r; + + assert(m); + + m->deferred_auto_login_event_source = sd_event_source_unref(m->deferred_auto_login_event_source); + + r = sd_bus_emit_properties_changed( + m->bus, + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "AutoLogin", NULL); + if (r < 0) + log_warning_errno(r, "Failed to send AutoLogin property change event, ignoring: %m"); + + return 0; +} + +int bus_manager_emit_auto_login_changed(Manager *m) { + int r; + assert(m); + + if (m->deferred_auto_login_event_source) + return 0; + + if (!m->event) + return 0; + + if (IN_SET(sd_event_get_state(m->event), SD_EVENT_FINISHED, SD_EVENT_EXITING)) + return 0; + + r = sd_event_add_defer(m->event, &m->deferred_auto_login_event_source, on_deferred_auto_login, m); + if (r < 0) + return log_error_errno(r, "Failed to allocate auto login event source: %m"); + + r = sd_event_source_set_priority(m->deferred_auto_login_event_source, SD_EVENT_PRIORITY_IDLE+10); + if (r < 0) + log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m"); + + (void) sd_event_source_set_description(m->deferred_auto_login_event_source, "deferred-auto-login"); + return 1; +} diff --git a/src/home/homed-manager-bus.h b/src/home/homed-manager-bus.h new file mode 100644 index 0000000000000..40e1cc3d86d60 --- /dev/null +++ b/src/home/homed-manager-bus.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "sd-bus.h" + +extern const sd_bus_vtable manager_vtable[]; diff --git a/src/home/homed-manager.c b/src/home/homed-manager.c new file mode 100644 index 0000000000000..0c952c8b67dfd --- /dev/null +++ b/src/home/homed-manager.c @@ -0,0 +1,1676 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "btrfs-util.h" +#include "bus-common-errors.h" +#include "bus-error.h" +#include "bus-util.h" +#include "clean-ipc.h" +#include "conf-files.h" +#include "device-util.h" +#include "dirent-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-util.h" +#include "fs-util.h" +#include "gpt.h" +#include "home-util.h" +#include "homed-home-bus.h" +#include "homed-home.h" +#include "homed-manager-bus.h" +#include "homed-manager.h" +#include "homed-varlink.h" +#include "io-util.h" +#include "mkdir.h" +#include "process-util.h" +#include "random-util.h" +#include "socket-util.h" +#include "stat-util.h" +#include "strv.h" +#include "tmpfile-util.h" +#include "udev-util.h" +#include "user-record-sign.h" +#include "user-record-util.h" +#include "user-record.h" +#include "user-util.h" + +/* Where to look for private/public keys that are used to sign the user records. We are not using + * CONF_PATHS_NULSTR() here since we want to insert /var/lib/systemd/home/ in the middle. And we insert that + * since we want to auto-generate a persistent private/public key pair if we need to. */ +#define KEY_PATHS_NULSTR \ + "/etc/systemd/home/\0" \ + "/run/systemd/home/\0" \ + "/var/lib/systemd/home/\0" \ + "/usr/local/lib/systemd/home/\0" \ + "/usr/lib/systemd/home/\0" + +static bool uid_is_home(uid_t uid) { + return uid >= HOME_UID_MIN && uid <= HOME_UID_MAX; +} +/* Takes a value generated randomly or by hashing and turns it into a UID in the right range */ + +#define UID_CLAMP_INTO_HOME_RANGE(rnd) (((uid_t) (rnd) % (HOME_UID_MAX - HOME_UID_MIN + 1)) + HOME_UID_MIN) + +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_uid_hash_ops, void, trivial_hash_func, trivial_compare_func, Home, home_free); +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_name_hash_ops, char, string_hash_func, string_compare_func, Home, home_free); +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_worker_pid_hash_ops, void, trivial_hash_func, trivial_compare_func, Home, home_free); +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(homes_by_sysfs_hash_ops, char, path_hash_func, path_compare_func, Home, home_free); + +static int on_home_inotify(sd_event_source *s, const struct inotify_event *event, void *userdata); +static int manager_gc_images(Manager *m); +static int manager_enumerate_images(Manager *m); +static int manager_assess_image(Manager *m, int dir_fd, const char *dir_path, const char *dentry_name); +static void manager_revalidate_image(Manager *m, Home *h); + +static void manager_watch_home(Manager *m) { + struct statfs sfs; + int r; + + assert(m); + + m->inotify_event_source = sd_event_source_unref(m->inotify_event_source); + m->scan_slash_home = false; + + if (statfs("/home/", &sfs) < 0) { + log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno, + "Failed to statfs() /home/ directory, disabling automatic scanning."); + return; + } + + if (is_network_fs(&sfs)) { + log_info("/home/ is a network file system, disabling automatic scanning."); + return; + } + + if (is_fs_type(&sfs, AUTOFS_SUPER_MAGIC)) { + log_info("/home/ is on autofs, disabling automatic scanning."); + return; + } + + m->scan_slash_home = true; + + r = sd_event_add_inotify(m->event, &m->inotify_event_source, "/home/", IN_CREATE|IN_CLOSE_WRITE|IN_DELETE_SELF|IN_MOVE_SELF|IN_ONLYDIR|IN_MOVED_TO|IN_MOVED_FROM|IN_DELETE, on_home_inotify, m); + if (r < 0) + log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r, + "Failed to create inotify watch on /home/, ignoring."); + + (void) sd_event_source_set_description(m->inotify_event_source, "home-inotify"); +} + +static int on_home_inotify(sd_event_source *s, const struct inotify_event *event, void *userdata) { + Manager *m = userdata; + const char *e, *n; + + assert(m); + assert(event); + + if ((event->mask & (IN_Q_OVERFLOW|IN_MOVE_SELF|IN_DELETE_SELF|IN_IGNORED|IN_UNMOUNT)) != 0) { + + if (FLAGS_SET(event->mask, IN_Q_OVERFLOW)) + log_debug("/home/ inotify queue overflow, rescanning."); + else if (FLAGS_SET(event->mask, IN_MOVE_SELF)) + log_info("/home/ moved or renamed, recreating watch and rescanning."); + else if (FLAGS_SET(event->mask, IN_DELETE_SELF)) + log_info("/home/ deleted, recreating watch and rescanning."); + else if (FLAGS_SET(event->mask, IN_UNMOUNT)) + log_info("/home/ unmounted, recreating watch and rescanning."); + else if (FLAGS_SET(event->mask, IN_IGNORED)) + log_info("/home/ watch invalidated, recreating watch and rescanning."); + + manager_watch_home(m); + (void) manager_gc_images(m); + (void) manager_enumerate_images(m); + (void) bus_manager_emit_auto_login_changed(m); + return 0; + } + + /* For the other inotify events, let's ignore all events for file names that don't match our + * expectations */ + if (isempty(event->name)) + return 0; + e = endswith(event->name, FLAGS_SET(event->mask, IN_ISDIR) ? ".homedir" : ".home"); + if (!e) + return 0; + + n = strndupa(event->name, e - event->name); + if (!suitable_user_name(n)) + return 0; + + if ((event->mask & (IN_CREATE|IN_CLOSE_WRITE|IN_MOVED_TO)) != 0) { + if (FLAGS_SET(event->mask, IN_CREATE)) + log_debug("/home/%s has been created, having a look.", event->name); + else if (FLAGS_SET(event->mask, IN_CLOSE_WRITE)) + log_debug("/home/%s has been modified, having a look.", event->name); + else if (FLAGS_SET(event->mask, IN_MOVED_TO)) + log_debug("/home/%s has been moved in, having a look.", event->name); + + (void) manager_assess_image(m, -1, "/home/", event->name); + (void) bus_manager_emit_auto_login_changed(m); + } + + if ((event->mask & (IN_DELETE|IN_MOVED_FROM|IN_DELETE)) != 0) { + Home *h; + + if (FLAGS_SET(event->mask, IN_DELETE)) + log_debug("/home/%s has been deleted, revalidating.", event->name); + else if (FLAGS_SET(event->mask, IN_CLOSE_WRITE)) + log_debug("/home/%s has been closed after writing, revalidating.", event->name); + else if (FLAGS_SET(event->mask, IN_MOVED_FROM)) + log_debug("/home/%s has been moved away, revalidating.", event->name); + + h = hashmap_get(m->homes_by_name, n); + if (h) { + manager_revalidate_image(m, h); + (void) bus_manager_emit_auto_login_changed(m); + } + } + + return 0; +} + +int manager_new(Manager **ret) { + _cleanup_(manager_freep) Manager *m = NULL; + int r; + + assert(ret); + + m = new0(Manager, 1); + if (!m) + return -ENOMEM; + + r = sd_event_default(&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->homes_by_uid = hashmap_new(&homes_by_uid_hash_ops); + if (!m->homes_by_uid) + return -ENOMEM; + + m->homes_by_name = hashmap_new(&homes_by_name_hash_ops); + if (!m->homes_by_name) + return -ENOMEM; + + m->homes_by_worker_pid = hashmap_new(&homes_by_worker_pid_hash_ops); + if (!m->homes_by_worker_pid) + return -ENOMEM; + + m->homes_by_sysfs = hashmap_new(&homes_by_sysfs_hash_ops); + if (!m->homes_by_sysfs) + return -ENOMEM; + + *ret = TAKE_PTR(m); + return 0; +} + +Manager* manager_free(Manager *m) { + assert(m); + + hashmap_free(m->homes_by_uid); + hashmap_free(m->homes_by_name); + hashmap_free(m->homes_by_worker_pid); + hashmap_free(m->homes_by_sysfs); + + m->inotify_event_source = sd_event_source_unref(m->inotify_event_source); + + bus_verify_polkit_async_registry_free(m->polkit_registry); + + sd_bus_flush_close_unref(m->bus); + sd_event_unref(m->event); + + m->notify_socket_event_source = sd_event_source_unref(m->notify_socket_event_source); + m->device_monitor = sd_device_monitor_unref(m->device_monitor); + + m->deferred_rescan_event_source = sd_event_source_unref(m->deferred_rescan_event_source); + m->deferred_gc_event_source = sd_event_source_unref(m->deferred_gc_event_source); + m->deferred_auto_login_event_source = sd_event_source_unref(m->deferred_auto_login_event_source); + + if (m->private_key) + EVP_PKEY_free(m->private_key); + + hashmap_free(m->public_keys); + + varlink_server_unref(m->varlink_server); + + return mfree(m); +} + +int manager_verify_user_record(Manager *m, UserRecord *hr) { + EVP_PKEY *pkey; + Iterator i; + int r; + + assert(m); + assert(hr); + + if (!m->private_key && hashmap_isempty(m->public_keys)) { + r = user_record_has_signature(hr); + if (r < 0) + return r; + + return r ? -ENOKEY : USER_RECORD_UNSIGNED; + } + + /* Is it our own? */ + if (m->private_key) { + r = user_record_verify(hr, m->private_key); + switch (r) { + + case USER_RECORD_FOREIGN: + /* This record is not signed by this key, but let's see below */ + break; + + case USER_RECORD_SIGNED: /* Signed by us, but also by others, let's propagate that */ + case USER_RECORD_SIGNED_EXCLUSIVE: /* Signed by us, and nothing else, ditto */ + case USER_RECORD_UNSIGNED: /* Not signed at all, ditto */ + default: + return r; + } + } + + HASHMAP_FOREACH(pkey, m->public_keys, i) { + r = user_record_verify(hr, pkey); + switch (r) { + + case USER_RECORD_FOREIGN: + /* This record is not signed by this key, but let's see our other keys */ + break; + + case USER_RECORD_SIGNED: /* It's signed by this key we are happy with, but which is not our own. */ + case USER_RECORD_SIGNED_EXCLUSIVE: + return USER_RECORD_FOREIGN; + + case USER_RECORD_UNSIGNED: /* It's not signed at all */ + default: + return r; + } + } + + return -ENOKEY; +} + +static int manager_add_home_by_record( + Manager *m, + const char *name, + int dir_fd, + const char *fname) { + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr; + unsigned line, column; + int r, is_signed; + Home *h; + + assert(m); + assert(name); + assert(fname); + + r = json_parse_file_at(NULL, dir_fd, fname, JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse identity record at %s:%u%u: %m", fname, line, column); + + hr = user_record_new(); + if (!hr) + return log_oom(); + + r = user_record_load(hr, v, USER_RECORD_LOAD_REFUSE_SECRET); + if (r < 0) + return r; + + if (!streq_ptr(hr->user_name, name)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Identity's user name %s does not match file name %s, refusing.", hr->user_name, name); + + is_signed = manager_verify_user_record(m, hr); + switch (is_signed) { + + case -ENOKEY: + return log_warning_errno(is_signed, "User record %s is not signed by any accepted key, ignoring.", fname); + case USER_RECORD_UNSIGNED: + return log_warning_errno(SYNTHETIC_ERRNO(EPERM), "User record %s is not signed at all, ignoring.", fname); + case USER_RECORD_SIGNED: + log_info("User record %s is signed by us (and others), accepting.", fname); + break; + case USER_RECORD_SIGNED_EXCLUSIVE: + log_info("User record %s is signed only by us, accepting.", fname); + break; + case USER_RECORD_FOREIGN: + log_info("User record %s is signed by registered key from others, accepting.", fname); + break; + default: + assert(is_signed < 0); + return log_error_errno(is_signed, "Failed to verify signature of user record in %s: %m", fname); + } + + h = hashmap_get(m->homes_by_name, name); + if (h) { + r = home_set_record(h, hr); + if (r < 0) + return log_error_errno(r, "Failed to update home record for %s: %m", name); + + /* If we acquired a record now for a previously unallocated entry, then reset the state. This + * makes sure home_get_state() will check for the availability of the image file dynamically + * in order to detect to distuingish HOME_INACTIVE and HOME_ABSENT. */ + if (h->state == HOME_UNFIXATED) + h->state = _HOME_STATE_INVALID; + } else { + r = home_new(m, hr, NULL, &h); + if (r < 0) + return log_error_errno(r, "Failed to allocate new home object: %m"); + + log_info("Added registered home for user %s.", hr->user_name); + } + + /* Only entries we exclusively signed are writable to us, hence remember the result */ + h->signed_locally = is_signed == USER_RECORD_SIGNED_EXCLUSIVE; + + return 1; +} + +static int manager_enumerate_records(Manager *m) { + _cleanup_closedir_ DIR *d = NULL; + struct dirent *de; + + assert(m); + + d = opendir("/var/lib/systemd/home/"); + if (!d) + return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno, + "Failed to open /var/lib/systemd/home/: %m"); + + FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read record directory: %m")) { + _cleanup_free_ char *n = NULL; + const char *e; + + if (!dirent_is_file(de)) + continue; + + e = endswith(de->d_name, ".identity"); + if (!e) + continue; + + n = strndup(de->d_name, e - de->d_name); + if (!n) + return log_oom(); + + if (!suitable_user_name(n)) + continue; + + (void) manager_add_home_by_record(m, n, dirfd(d), de->d_name); + } + + return 0; +} + +static int search_quota(uid_t uid, const char *exclude_quota_path) { + struct stat exclude_st = {}; + dev_t previous_devno = 0; + const char *where; + int r; + + /* Checks whether the specified UID owns any files on the files system, but ignore any file system + * backing the specified file. The file is used when operating on home directories, where it's OK if + * the UID of them already owns files. */ + + if (exclude_quota_path && stat(exclude_quota_path, &exclude_st) < 0) { + if (errno != ENOENT) + return log_warning_errno(errno, "Failed to stat %s, ignoring: %m", exclude_quota_path); + } + + /* Check a few usual suspects where regular users might own files. Note that this is by no means + * comprehensive, but should cover most cases. Note that in an ideal world every user would be + * registered in NSS and avoid our own UID range, but for all other cases, it's a good idea to be + * paranoid and check quota if we can. */ + FOREACH_STRING(where, "/home/", "/tmp/", "/var/", "/var/mail/", "/var/tmp/", "/var/spool/") { + _cleanup_free_ char *devnode = NULL; + struct dqblk req; + struct stat st; + + if (stat(where, &st) < 0) { + log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno, + "Failed to stat %s, ignoring: %m", where); + continue; + } + + if (major(st.st_dev) == 0) { + log_debug("Directory %s is not on a real block device, not checking quota for UID use.", where); + continue; + } + + if (st.st_dev == exclude_st.st_dev) { /* If an exclude path is specified, then ignore quota + * reported on the same block device as that path. */ + log_debug("Directory %s is where the home directory is located, not checking quota for UID use.", where); + continue; + } + + if (st.st_dev == previous_devno) { /* Does this directory have the same devno as the previous + * one we tested? If so, there's no point in testing this + * again. */ + log_debug("Directory %s is on same device as previous tested directory, not checking quota for UID use a second time.", where); + continue; + } + + previous_devno = st.st_dev; + + r = device_path_make_major_minor(S_IFBLK, st.st_dev, &devnode); + if (r < 0) + return log_error_errno(r, "Failed to derive block device path for %s: %m", where); + + if (quotactl(QCMD(Q_GETQUOTA, USRQUOTA), devnode, uid, (caddr_t) &req) < 0) { + if (ERRNO_IS_NOT_SUPPORTED(errno)) + log_debug_errno(errno, "No UID quota support on %s, ignoring.", where); + else + log_warning_errno(errno, "Failed to query quota on %s, ignoring.", where); + + continue; + } + + if ((FLAGS_SET(req.dqb_valid, QIF_SPACE) && req.dqb_curspace > 0) || + (FLAGS_SET(req.dqb_valid, QIF_INODES) && req.dqb_curinodes > 0)) { + log_debug_errno(errno, "Quota reports UID " UID_FMT " occupies disk space on %s.", uid, where); + return 1; + } + } + + return 0; +} + +static int manager_acquire_uid( + Manager *m, + uid_t start_uid, + const char *user_name, + const char *exclude_quota_path, + uid_t *ret) { + + static const uint8_t hash_key[] = { + 0xa3, 0xb8, 0x82, 0x69, 0x9a, 0x71, 0xf7, 0xa9, + 0xe0, 0x7c, 0xf6, 0xf1, 0x21, 0x69, 0xd2, 0x1e + }; + + enum { + PHASE_SUGGESTED, + PHASE_HASHED, + PHASE_RANDOM + } phase = PHASE_SUGGESTED; + + unsigned n_tries = 100; + int r; + + assert(m); + assert(ret); + + for (;;) { + struct passwd *pw; + struct group *gr; + uid_t candidate; + Home *other; + + if (--n_tries <= 0) + return -EBUSY; + + switch (phase) { + + case PHASE_SUGGESTED: + phase = PHASE_HASHED; + + if (!uid_is_home(start_uid)) + continue; + + candidate = start_uid; + break; + + case PHASE_HASHED: + phase = PHASE_RANDOM; + + if (!user_name) + continue; + + candidate = UID_CLAMP_INTO_HOME_RANGE(siphash24(user_name, strlen(user_name), hash_key)); + break; + + case PHASE_RANDOM: + random_bytes(&candidate, sizeof(candidate)); + candidate = UID_CLAMP_INTO_HOME_RANGE(candidate); + break; + + default: + assert_not_reached("unknown phase"); + } + + other = hashmap_get(m->homes_by_uid, UID_TO_PTR(candidate)); + if (other) { + log_debug("Candidate UID " UID_FMT " already used by another home directory (%s), let's try another.", candidate, other->user_name); + continue; + } + + pw = getpwuid(candidate); + if (pw) { + log_debug("Candidate UID " UID_FMT " already registered by another user in NSS (%s), let's try another.", candidate, pw->pw_name); + continue; + } + + gr = getgrgid((gid_t) candidate); + if (gr) { + log_debug("Candidate UID " UID_FMT " already registered by another group in NSS (%s), let's try another.", candidate, gr->gr_name); + continue; + } + + r = search_ipc(candidate, (gid_t) candidate); + if (r < 0) + continue; + if (r > 0) { + log_debug_errno(r, "Candidate UID " UID_FMT " already owns IPC objects, let's try another: %m", candidate); + continue; + } + + r = search_quota(candidate, exclude_quota_path); + if (r != 0) + continue; + + *ret = candidate; + return 0; + } +} + +static int manager_add_home_by_image( + Manager *m, + const char *user_name, + const char *realm, + const char *image_path, + const char *sysfs, + UserStorage storage, + uid_t start_uid) { + + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + uid_t uid; + Home *h; + int r; + + assert(m); + + assert(m); + assert(user_name); + assert(image_path); + assert(storage >= 0); + assert(storage < _USER_STORAGE_MAX); + + h = hashmap_get(m->homes_by_name, user_name); + if (h) { + bool same; + + if (h->state != HOME_UNFIXATED) { + log_debug("Found an image for user %s which already has a record, skipping.", user_name); + return 0; /* ignore images that synthesize a user we already have a record for */ + } + + same = user_record_storage(h->record) == storage; + if (same) { + if (h->sysfs && sysfs) + same = path_equal(h->sysfs, sysfs); + else if (!!h->sysfs != !!sysfs) + same = false; + else { + const char *p; + + p = user_record_image_path(h->record); + same = p && path_equal(p, image_path); + } + } + + if (!same) { + log_debug("Found a multiple images for a user '%s', ignoring image '%s'.", user_name, image_path); + return 0; + } + } else { + /* Check NSS, in case there's another user or group by this name */ + if (getpwnam(user_name) || getgrnam(user_name)) { + log_debug("Found an existing user or group by name '%s', ignoring image '%s'.", user_name, image_path); + return 0; + } + } + + if (h && uid_is_valid(h->uid)) + uid = h->uid; + else { + r = manager_acquire_uid(m, start_uid, user_name, IN_SET(storage, USER_SUBVOLUME, USER_DIRECTORY, USER_FSCRYPT) ? image_path : NULL, &uid); + if (r < 0) + return log_warning_errno(r, "Failed to acquire unused UID for %s: %m", user_name); + } + + hr = user_record_new(); + if (!hr) + return log_oom(); + + r = user_record_synthesize(hr, user_name, realm, image_path, storage, uid, (gid_t) uid); + if (r < 0) + return log_error_errno(r, "Failed to synthesize home record for %s (image %s): %m", user_name, image_path); + + if (h) { + r = home_set_record(h, hr); + if (r < 0) + return log_error_errno(r, "Failed to update home record for %s: %m", user_name); + } else { + r = home_new(m, hr, sysfs, &h); + if (r < 0) + return log_error_errno(r, "Failed to allocate new home object: %m"); + + h->state = HOME_UNFIXATED; + + log_info("Discovered new home for user %s through image %s.", user_name, image_path); + } + + return 1; +} + +int manager_augment_record_with_uid( + Manager *m, + UserRecord *hr) { + + const char *exclude_quota_path = NULL; + uid_t start_uid = UID_INVALID, uid; + int r; + + assert(m); + assert(hr); + + if (uid_is_valid(hr->uid)) + return 0; + + if (IN_SET(hr->storage, USER_CLASSIC, USER_SUBVOLUME, USER_DIRECTORY, USER_FSCRYPT)) { + const char * ip; + + ip = user_record_image_path(hr); + if (ip) { + struct stat st; + + if (stat(ip, &st) < 0) { + if (errno != ENOENT) + log_warning_errno(errno, "Failed to stat(%s): %m", ip); + } else if (uid_is_home(st.st_uid)) { + start_uid = st.st_uid; + exclude_quota_path = ip; + } + } + } + + r = manager_acquire_uid(m, start_uid, hr->user_name, exclude_quota_path, &uid); + if (r < 0) + return r; + + log_debug("Acquired new UID " UID_FMT " for %s.", uid, hr->user_name); + + r = user_record_add_binding( + hr, + _USER_STORAGE_INVALID, + NULL, + SD_ID128_NULL, + SD_ID128_NULL, + SD_ID128_NULL, + NULL, + NULL, + UINT64_MAX, + NULL, + NULL, 0, + NULL, + uid, + (gid_t) uid); + if (r < 0) + return r; + + return 1; +} + +static int manager_assess_image( + Manager *m, + int dir_fd, + const char *dir_path, + const char *dentry_name) { + + char *luks_suffix, *directory_suffix; + _cleanup_free_ char *path = NULL; + struct stat st; + int r; + + assert(m); + assert(dir_path); + assert(dentry_name); + + luks_suffix = endswith(dentry_name, ".home"); + if (luks_suffix) + directory_suffix = NULL; + else + directory_suffix = endswith(dentry_name, ".homedir"); + + /* Early filter out: by name */ + if (!luks_suffix && !directory_suffix) + return 0; + + path = path_join(dir_path, dentry_name); + if (!path) + return log_oom(); + + /* Follow symlinks here, to allow people to link in stuff to make them available locally. */ + if (dir_fd >= 0) + r = fstatat(dir_fd, dentry_name, &st, 0); + else + r = stat(path, &st); + if (r < 0) + return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno, + "Failed to stat directory entry '%s', ignoring: %m", dentry_name); + + if (S_ISREG(st.st_mode)) { + _cleanup_free_ char *n = NULL, *user_name = NULL, *realm = NULL; + + if (!luks_suffix) + return 0; + + n = strndup(dentry_name, luks_suffix - dentry_name); + if (!n) + return log_oom(); + + r = split_user_name_realm(n, &user_name, &realm); + if (r == -EINVAL) /* Not the right format: ignore */ + return 0; + if (r < 0) + return log_error_errno(r, "Failed to split image name into user name/realm: %m"); + + return manager_add_home_by_image(m, user_name, realm, path, NULL, USER_LUKS, UID_INVALID); + } + + if (S_ISDIR(st.st_mode)) { + _cleanup_free_ char *n = NULL, *user_name = NULL, *realm = NULL; + _cleanup_close_ int fd = -1; + UserStorage storage; + + if (!directory_suffix) + return 0; + + n = strndup(dentry_name, directory_suffix - dentry_name); + if (!n) + return log_oom(); + + r = split_user_name_realm(n, &user_name, &realm); + if (r == -EINVAL) /* Not the right format: ignore */ + return 0; + if (r < 0) + return log_error_errno(r, "Failed to split image name into user name/realm: %m"); + + if (dir_fd >= 0) + fd = openat(dir_fd, dentry_name, O_DIRECTORY|O_RDONLY|O_CLOEXEC); + else + fd = open(path, O_DIRECTORY|O_RDONLY|O_CLOEXEC); + if (fd < 0) + return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno, + "Failed to open directory '%s', ignoring: %m", path); + + if (fstat(fd, &st) < 0) + return log_warning_errno(errno, "Failed to fstat() %s, ignoring: %m", path); + + assert(S_ISDIR(st.st_mode)); /* Must hold, we used O_DIRECTORY above */ + + r = btrfs_is_subvol_fd(fd); + if (r < 0) + return log_warning_errno(errno, "Failed to determine whether %s is a btrfs subvolume: %m", path); + if (r > 0) + storage = USER_SUBVOLUME; + else { + struct fscrypt_policy policy; + + if (ioctl(fd, FS_IOC_GET_ENCRYPTION_POLICY, &policy) < 0) { + + if (errno == ENODATA) + log_debug_errno(errno, "Determined %s is not fscrypt encrypted.", path); + else if (ERRNO_IS_NOT_SUPPORTED(errno)) + log_debug_errno(errno, "Determined %s is not fscrypt encrypted because kernel or file system don't support it.", path); + else + log_debug_errno(errno, "FS_IOC_GET_ENCRYPTION_POLICY failed with unexpected error code on %s, ignoring: %m", path); + + storage = USER_DIRECTORY; + } else + storage = USER_FSCRYPT; + } + + return manager_add_home_by_image(m, user_name, realm, path, NULL, storage, st.st_uid); + } + + return 0; +} + +int manager_enumerate_images(Manager *m) { + _cleanup_closedir_ DIR *d = NULL; + struct dirent *de; + + assert(m); + + if (!m->scan_slash_home) + return 0; + + d = opendir("/home/"); + if (!d) + return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_ERR, errno, + "Failed to open /home/: %m"); + + FOREACH_DIRENT(de, d, return log_error_errno(errno, "Failed to read /home/ directory: %m")) + (void) manager_assess_image(m, dirfd(d), "/home", de->d_name); + + return 0; +} + +static int manager_connect_bus(Manager *m) { + int r; + + assert(m); + assert(!m->bus); + + r = sd_bus_default_system(&m->bus); + if (r < 0) + return log_error_errno(r, "Failed to connect to system bus: %m"); + + r = sd_bus_add_object_vtable(m->bus, NULL, "/org/freedesktop/home1", "org.freedesktop.home1.Manager", manager_vtable, m); + if (r < 0) + return log_error_errno(r, "Failed to add manager object vtable: %m"); + + r = sd_bus_add_fallback_vtable(m->bus, NULL, "/org/freedesktop/home1/home", "org.freedesktop.home1.Home", home_vtable, bus_home_object_find, m); + if (r < 0) + return log_error_errno(r, "Failed to add image object vtable: %m"); + + r = sd_bus_add_node_enumerator(m->bus, NULL, "/org/freedesktop/home1/home", bus_home_node_enumerator, m); + if (r < 0) + return log_error_errno(r, "Failed to add image enumerator: %m"); + + r = sd_bus_add_object_manager(m->bus, NULL, "/org/freedesktop/home1/home"); + if (r < 0) + return log_error_errno(r, "Failed to add object manager: %m"); + + r = sd_bus_request_name_async(m->bus, NULL, "org.freedesktop.home1", 0, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to request name: %m"); + + r = sd_bus_attach_event(m->bus, m->event, 0); + if (r < 0) + return log_error_errno(r, "Failed to attach bus to event loop: %m"); + + (void) sd_bus_set_exit_on_disconnect(m->bus, true); + + return 0; +} + +static int manager_bind_varlink(Manager *m) { + int r; + + assert(m); + assert(!m->varlink_server); + + r = varlink_server_new(&m->varlink_server, VARLINK_SERVER_ACCOUNT_UID); + if (r < 0) + return log_error_errno(r, "Failed to allocate varlink server object: %m"); + + varlink_server_set_userdata(m->varlink_server, m); + + r = varlink_server_bind_method_many( + m->varlink_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 register varlink methods: %m"); + + (void) mkdir_p("/run/systemd/userdb", 0755); + + r = varlink_server_listen_address(m->varlink_server, "/run/systemd/userdb/io.systemd.Home", 0666); + if (r < 0) + return log_error_errno(r, "Failed to bind to varlink socket: %m"); + + r = varlink_server_attach_event(m->varlink_server, m->event, SD_EVENT_PRIORITY_NORMAL); + if (r < 0) + return log_error_errno(r, "Failed to attach varlink connection to event loop: %m"); + + return 0; +} + +static ssize_t read_datagram(int fd, struct ucred *ret_sender, void **ret) { + _cleanup_free_ void *buffer = NULL; + ssize_t n, m; + + assert(fd >= 0); + assert(ret_sender); + assert(ret); + + n = next_datagram_size_fd(fd); + if (n < 0) + return n; + + buffer = malloc(n + 2); + if (!buffer) + return -ENOMEM; + + if (ret_sender) { + union { + struct cmsghdr cmsghdr; + uint8_t buf[CMSG_SPACE(sizeof(struct ucred))]; + } control; + bool found_ucred = false; + struct cmsghdr *cmsg; + struct msghdr mh; + struct iovec iov; + + /* Pass one extra byte, as a size check */ + iov = IOVEC_MAKE(buffer, n + 1); + + mh = (struct msghdr) { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = &control, + .msg_controllen = sizeof(control), + }; + + m = recvmsg(fd, &mh, MSG_DONTWAIT|MSG_CMSG_CLOEXEC); + if (m < 0) + return -errno; + + cmsg_close_all(&mh); + + /* Ensure the size matches what we determined before */ + if (m != n) + return -EMSGSIZE; + + CMSG_FOREACH(cmsg, &mh) + if (cmsg->cmsg_level == SOL_SOCKET && + cmsg->cmsg_type == SCM_CREDENTIALS && + cmsg->cmsg_len == CMSG_LEN(sizeof(struct ucred))) { + + memcpy(ret_sender, CMSG_DATA(cmsg), sizeof(struct ucred)); + found_ucred = true; + } + + if (!found_ucred) + *ret_sender = (struct ucred) { + .pid = 0, + .uid = UID_INVALID, + .gid = GID_INVALID, + }; + } else { + m = recv(fd, buffer, n + 1, MSG_DONTWAIT); + if (m < 0) + return -errno; + + /* Ensure the size matches what we determined before */ + if (m != n) + return -EMSGSIZE; + } + + /* For safety reasons: let's always NUL terminate. */ + ((char*) buffer)[n] = 0; + *ret = TAKE_PTR(buffer); + + return 0; +} + +static int on_notify_socket(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + _cleanup_strv_free_ char **l = NULL; + _cleanup_free_ void *datagram = NULL; + struct ucred sender; + Manager *m = userdata; + ssize_t n; + Home *h; + + assert(s); + assert(m); + + n = read_datagram(fd, &sender, &datagram); + if (IN_SET(n, -EAGAIN, -EINTR)) + return 0; + if (n < 0) + return log_error_errno(n, "Failed to read notify datagram: %m"); + + if (sender.pid <= 0) { + log_warning("Received notify datagram without valid sender PID, ignoring."); + return 0; + } + + h = hashmap_get(m->homes_by_worker_pid, PID_TO_PTR(sender.pid)); + if (!h) { + log_warning("Recieved notify datagram of unknown process, ignoring."); + return 0; + } + + l = strv_split(datagram, "\n"); + if (!l) + return log_oom(); + + home_process_notify(h, l); + return 0; +} + +static int manager_listen_notify(Manager *m) { + _cleanup_close_ int fd = -1; + union sockaddr_union sa; + int r; + + assert(m); + assert(!m->notify_socket_event_source); + + fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + if (fd < 0) + return log_error_errno(errno, "Failed to create listening socket: %m"); + + r = sockaddr_un_set_path(&sa.un, "/run/systemd/home/notify"); + if (r < 0) + return log_error_errno(r, "Failed to set AF_UNIX socket path: %m"); + + (void) mkdir_parents(sa.un.sun_path, 0755); + (void) sockaddr_un_unlink(&sa.un); + + if (bind(fd, &sa.sa, SOCKADDR_UN_LEN(sa.un)) < 0) + return log_error_errno(errno, "Failed to bind to socket: %m"); + + r = setsockopt_int(fd, SOL_SOCKET, SO_PASSCRED, true); + if (r < 0) + return r; + + r = sd_event_add_io(m->event, &m->notify_socket_event_source, fd, EPOLLIN, on_notify_socket, m); + if (r < 0) + return log_error_errno(r, "Failed to allocate event source for notify socket: %m"); + + (void) sd_event_source_set_description(m->notify_socket_event_source, "notify-socket"); + + /* Make sure we process sd_notify() before SIGCHLD for any worker, so that we always know the error + * number of a client before it exits. */ + r = sd_event_source_set_priority(m->notify_socket_event_source, SD_EVENT_PRIORITY_NORMAL - 5); + if (r < 0) + return log_error_errno(r, "Failed to alter priority of NOTIFY_SOCKET event source: %m"); + + r = sd_event_source_set_io_fd_own(m->notify_socket_event_source, true); + if (r < 0) + return log_error_errno(r, "Failed to pass ownership of notify socket: %m"); + + return TAKE_FD(fd); +} + +static int manager_add_device(Manager *m, sd_device *d) { + _cleanup_free_ char *user_name = NULL, *realm = NULL, *node = NULL; + const char *tabletype, *parttype, *partname, *partuuid, *sysfs; + sd_id128_t id; + int r; + + assert(m); + assert(d); + + r = sd_device_get_syspath(d, &sysfs); + if (r < 0) + return log_error_errno(r, "Failed to acquire sysfs path of device: %m"); + + r = sd_device_get_property_value(d, "ID_PART_TABLE_TYPE", &tabletype); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_error_errno(r, "Failed to acquire ID_PART_TABLE_TYPE device property, ignoring: %m"); + + if (!streq(tabletype, "gpt")) { + log_debug("Found partition (%s) on non-GPT table, ignoring.", sysfs); + return 0; + } + + r = sd_device_get_property_value(d, "ID_PART_ENTRY_TYPE", &parttype); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_error_errno(r, "Failed to acquire ID_PART_ENTRY_TYPE device property, ignoring: %m"); + r = sd_id128_from_string(parttype, &id); + if (r < 0) + return log_debug_errno(r, "Failed to parse ID_PART_ENTRY_TYPE field '%s', ignoring: %m", parttype); + if (!sd_id128_equal(id, GPT_USER_HOME)) { + log_debug("Found partition (%s) we don't care about, ignoring.", sysfs); + return 0; + } + + r = sd_device_get_property_value(d, "ID_PART_ENTRY_NAME", &partname); + if (r < 0) + return log_warning_errno(r, "Failed to acquire ID_PART_ENTRY_NAME device property, ignoring: %m"); + + r = split_user_name_realm(partname, &user_name, &realm); + if (r == -EINVAL) + return log_warning_errno(r, "Found partition with correct partition type but a non-parsable partition name '%s', ignoring.", partname); + if (r < 0) + return log_error_errno(r, "Failed to validate partition name '%s': %m", partname); + + r = sd_device_get_property_value(d, "ID_FS_UUID", &partuuid); + if (r < 0) + return log_warning_errno(r, "Failed to acquire ID_FS_UUID device property, ignoring: %m"); + + r = sd_id128_from_string(partuuid, &id); + if (r < 0) + return log_warning_errno(r, "Failed to parse ID_FS_UUID field '%s', ignoring: %m", partuuid); + + if (asprintf(&node, "/dev/disk/by-uuid/" SD_ID128_UUID_FORMAT_STR, SD_ID128_FORMAT_VAL(id)) < 0) + return log_oom(); + + return manager_add_home_by_image(m, user_name, realm, node, sysfs, USER_LUKS, UID_INVALID); +} + +static int manager_on_device(sd_device_monitor *monitor, sd_device *d, void *userdata) { + Manager *m = userdata; + int r; + + assert(m); + assert(d); + + if (device_for_action(d, DEVICE_ACTION_REMOVE)) { + const char *sysfs; + Home *h; + + r = sd_device_get_syspath(d, &sysfs); + if (r < 0) { + log_warning_errno(r, "Failed to acquire sysfs path from device: %m"); + return 0; + } + + log_info("block device %s has been removed.", sysfs); + + /* Let's see if we previously synthesized a home record from this device, if so, let's just + * revalidate that. Otherwise let's revalidate them all, but asynchronously. */ + h = hashmap_get(m->homes_by_sysfs, sysfs); + if (h) + manager_revalidate_image(m, h); + else + manager_enqueue_gc(m, NULL); + } else + (void) manager_add_device(m, d); + + (void) bus_manager_emit_auto_login_changed(m); + return 0; +} + +static int manager_watch_devices(Manager *m) { + int r; + + assert(m); + assert(!m->device_monitor); + + r = sd_device_monitor_new(&m->device_monitor); + if (r < 0) + return log_error_errno(r, "Failed to allocate device monitor: %m"); + + r = sd_device_monitor_filter_add_match_subsystem_devtype(m->device_monitor, "block", NULL); + if (r < 0) + return log_error_errno(r, "Failed to configure device monitor match: %m"); + + r = sd_device_monitor_attach_event(m->device_monitor, m->event); + if (r < 0) + return log_error_errno(r, "Failed to attach device monitor to event loop: %m"); + + r = sd_device_monitor_start(m->device_monitor, manager_on_device, m); + if (r < 0) + return log_error_errno(r, "Failed to start device monitor: %m"); + + return 0; +} + +static int manager_enumerate_devices(Manager *m) { + _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; + sd_device *d; + int r; + + assert(m); + + r = sd_device_enumerator_new(&e); + if (r < 0) + return r; + + r = sd_device_enumerator_add_match_subsystem(e, "block", true); + if (r < 0) + return r; + + FOREACH_DEVICE(e, d) + (void) manager_add_device(m, d); + + return 0; +} + +static int manager_load_key_pair(Manager *m) { + _cleanup_(fclosep) FILE *f = NULL; + struct stat st; + int r; + + assert(m); + + if (m->private_key) { + EVP_PKEY_free(m->private_key); + m->private_key = NULL; + } + + r = search_and_fopen_nulstr("local.private", "re", NULL, KEY_PATHS_NULSTR, &f); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_error_errno(r, "Failed to read private key file: %m"); + + if (fstat(fileno(f), &st) < 0) + return log_error_errno(errno, "Failed to stat private key file: %m"); + + r = stat_verify_regular(&st); + if (r < 0) + return log_error_errno(r, "Private key file is not regular: %m"); + + if (st.st_uid != 0 || (st.st_mode & 0077) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Private key file is readable by more than the root user"); + + m->private_key = PEM_read_PrivateKey(f, NULL, NULL, NULL); + if (!m->private_key) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to load private key pair"); + + log_info("Successfully loaded private key pair."); + + return 1; +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_PKEY_CTX*, EVP_PKEY_CTX_free); + +static int manager_generate_key_pair(Manager *m) { + _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx = NULL; + _cleanup_(unlink_and_freep) char *temp_public = NULL, *temp_private = NULL; + _cleanup_fclose_ FILE *fpublic = NULL, *fprivate = NULL; + int r; + + if (m->private_key) { + EVP_PKEY_free(m->private_key); + m->private_key = NULL; + } + + ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL); + if (!ctx) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to allocate Ed25519 key generation context."); + + if (EVP_PKEY_keygen_init(ctx) <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to initialize Ed25519 key generation context."); + + log_info("Generating key pair for signing local user identity records."); + + if (EVP_PKEY_keygen(ctx, &m->private_key) <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to generate Ed25519 key pair"); + + log_info("Successfully created Ed25519 key pair."); + + (void) mkdir_p("/var/lib/systemd/home", 0755); + + /* Write out public key (note that we only do that as a help to the user, we don't make use of this ever */ + r = fopen_temporary("/var/lib/systemd/home/local.public", &fpublic, &temp_public); + if (r < 0) + return log_error_errno(errno, "Failed ot open key file for writing: %m"); + + if (PEM_write_PUBKEY(fpublic, m->private_key) <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write public key."); + + r = fflush_and_check(fpublic); + if (r < 0) + return log_error_errno(r, "Failed to write private key: %m"); + + fpublic = safe_fclose(fpublic); + + /* Write out the private key (this actually writes out both private and public, OpenSSL is confusing) */ + r = fopen_temporary("/var/lib/systemd/home/local.private", &fprivate, &temp_private); + if (r < 0) + return log_error_errno(errno, "Failed ot open key file for writing: %m"); + + if (PEM_write_PrivateKey(fprivate, m->private_key, NULL, NULL, 0, NULL, 0) <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write private key pair."); + + r = fflush_and_check(fprivate); + if (r < 0) + return log_error_errno(r, "Failed to write private key: %m"); + + fprivate = safe_fclose(fprivate); + + /* Both are written now, move them into place */ + + if (rename(temp_public, "/var/lib/systemd/home/local.public") < 0) + return log_error_errno(errno, "Failed to move public key file into place: %m"); + temp_public = mfree(temp_public); + + if (rename(temp_private, "/var/lib/systemd/home/local.private") < 0) { + (void) unlink_noerrno("/var/lib/systemd/home/local.public"); /* try to remove the file we already created */ + return log_error_errno(errno, "Failed to move privtate key file into place: %m"); + } + temp_private = mfree(temp_private); + + return 1; +} + +int manager_acquire_key_pair(Manager *m) { + int r; + + assert(m); + + /* Already there? */ + if (m->private_key) + return 1; + + /* First try to load key off disk */ + r = manager_load_key_pair(m); + if (r != 0) + return r; + + /* Didn't work, generate a new one */ + return manager_generate_key_pair(m); +} + +int manager_sign_user_record(Manager *m, UserRecord *u, UserRecord **ret, sd_bus_error *error) { + int r; + + assert(m); + assert(u); + assert(ret); + + r = manager_acquire_key_pair(m); + if (r < 0) + return r; + if (r == 0) + return sd_bus_error_setf(error, BUS_ERROR_NO_PRIVATE_KEY, "Can't sign without local key."); + + return user_record_sign(u, m->private_key, ret); +} + +DEFINE_PRIVATE_HASH_OPS_FULL(public_key_hash_ops, char, string_hash_func, string_compare_func, free, EVP_PKEY, EVP_PKEY_free); +DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_PKEY*, EVP_PKEY_free); + +static int manager_load_public_key_one(Manager *m, const char *path) { + _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL; + _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char *fn = NULL; + struct stat st; + int r; + + assert(m); + + if (streq(basename(path), "local.public")) /* we already loaded the private key, which includes the public one */ + return 0; + + f = fopen(path, "re"); + if (!f) { + if (errno == ENOENT) + return 0; + + return log_error_errno(errno, "Failed to open public key %s: %m", path); + } + + if (fstat(fileno(f), &st) < 0) + return log_error_errno(errno, "Failed to stat public key %s: %m", path); + + r = stat_verify_regular(&st); + if (r < 0) + return log_error_errno(r, "Public key file %s is not a regular file: %m", path); + + if (st.st_uid != 0 || (st.st_mode & 0022) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Public key file %s is writable by more than the root user, refusing.", path); + + r = hashmap_ensure_allocated(&m->public_keys, &public_key_hash_ops); + if (r < 0) + return log_oom(); + + pkey = PEM_read_PUBKEY(f, &pkey, NULL, NULL); + if (!pkey) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse public key file %s.", path); + + fn = strdup(basename(path)); + if (!fn) + return log_oom(); + + r = hashmap_put(m->public_keys, fn, pkey); + if (r < 0) + return log_error_errno(r, "Failed to add public key to set: %m"); + + TAKE_PTR(fn); + TAKE_PTR(pkey); + + return 0; +} + +static int manager_load_public_keys(Manager *m) { + _cleanup_strv_free_ char **files = NULL; + char **i; + int r; + + assert(m); + + m->public_keys = hashmap_free(m->public_keys); + + r = conf_files_list_nulstr( + &files, + ".public", + NULL, + CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, + KEY_PATHS_NULSTR); + if (r < 0) + return log_error_errno(r, "Failed to assemble list of public key directories: %m"); + + STRV_FOREACH(i, files) + (void) manager_load_public_key_one(m, *i); + + return 0; +} + +int manager_startup(Manager *m) { + int r; + + assert(m); + + r = manager_listen_notify(m); + if (r < 0) + return r; + + r = manager_connect_bus(m); + if (r < 0) + return r; + + r = manager_bind_varlink(m); + if (r < 0) + return r; + + r = manager_load_key_pair(m); /* only try to load it, don't generate any */ + if (r < 0) + return r; + + r = manager_load_public_keys(m); + if (r < 0) + return r; + + manager_watch_home(m); + (void) manager_watch_devices(m); + + (void) manager_enumerate_records(m); + (void) manager_enumerate_images(m); + (void) manager_enumerate_devices(m); + + /* Let's clean up home directories whose devices got removed while we were not running */ + (void) manager_enqueue_gc(m, NULL); + + return 0; +} + +void manager_revalidate_image(Manager *m, Home *h) { + int r; + + assert(m); + assert(h); + + /* Frees an automatically discovered image, if it's synthetic and its image disappeared. Unmounts any + * image if it's mounted but it's image vanished. */ + + if (h->current_operation || !ordered_set_isempty(h->pending_operations)) + return; + + if (h->state == HOME_UNFIXATED) { + r = user_record_test_image_path(h->record); + if (r < 0) + log_warning_errno(r, "Can't determine if image of %s exists, freeing unfixated user: %m", h->user_name); + else if (r == USER_TEST_ABSENT) + log_info("Image for %s disappeared, freeing unfixated user.", h->user_name); + else + return; + + home_free(h); + + } else if (h->state < 0) { + + r = user_record_test_home_directory(h->record); + if (r < 0) { + log_warning_errno(r, "Unable to determine state of home directory, ignoring: %m"); + return; + } + + if (r == USER_TEST_MOUNTED) { + r = user_record_test_image_path(h->record); + if (r < 0) { + log_warning_errno(r, "Unable to determine state of image path, ignoring: %m"); + return; + } + + if (r == USER_TEST_ABSENT) { + _cleanup_(operation_unrefp) Operation *o = NULL; + + log_notice("Backing image disappeared while home directory %s was mounted, unmounting it forcibly.", h->user_name); + /* Wowza, the thing is mounted, but the device is gone? Act on it. */ + + r = home_killall(h); + if (r < 0) + log_warning_errno(r, "Failed to kill processes of user %s, ignoring: %m", h->user_name); + + /* We enqueue the operation here, after all the home directory might + * currently already run some operation, and we can deactivate it only after + * that's complete. */ + o = operation_new(OPERATION_DEACTIVATE_FORCE, NULL); + if (!o) { + log_oom(); + return; + } + + r = home_schedule_operation(h, o, NULL); + if (r < 0) + log_warning_errno(r, "Failed to enqueue forced home directory %s deactivation, ignoring: %m", h->user_name); + } + } + } +} + +int manager_gc_images(Manager *m) { + Home *h; + + assert_se(m); + + if (m->gc_focus) { + /* Focus on a specific home */ + + h = TAKE_PTR(m->gc_focus); + manager_revalidate_image(m, h); + } else { + /* Gc all */ + Iterator i; + + HASHMAP_FOREACH(h, m->homes_by_name, i) + manager_revalidate_image(m, h); + } + + return 0; +} + +static int on_deferred_rescan(sd_event_source *s, void *userdata) { + Manager *m = userdata; + + assert(m); + + m->deferred_rescan_event_source = sd_event_source_unref(m->deferred_rescan_event_source); + + manager_enumerate_devices(m); + manager_enumerate_images(m); + return 0; +} + +int manager_enqueue_rescan(Manager *m) { + int r; + + assert(m); + + if (m->deferred_rescan_event_source) + return 0; + + if (!m->event) + return 0; + + if (IN_SET(sd_event_get_state(m->event), SD_EVENT_FINISHED, SD_EVENT_EXITING)) + return 0; + + r = sd_event_add_defer(m->event, &m->deferred_rescan_event_source, on_deferred_rescan, m); + if (r < 0) + return log_error_errno(r, "Failed to allocate rescan event source: %m"); + + r = sd_event_source_set_priority(m->deferred_rescan_event_source, SD_EVENT_PRIORITY_IDLE+1); + if (r < 0) + log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m"); + + (void) sd_event_source_set_description(m->deferred_rescan_event_source, "deferred-rescan"); + return 1; +} + +static int on_deferred_gc(sd_event_source *s, void *userdata) { + Manager *m = userdata; + + assert(m); + + m->deferred_gc_event_source = sd_event_source_unref(m->deferred_gc_event_source); + + manager_gc_images(m); + return 0; +} + +int manager_enqueue_gc(Manager *m, Home *focus) { + int r; + + assert(m); + + /* This enqueues a request to GC dead homes. It may be called with focus=NULL in which case all homes + * will be scanned, or with the parameter set, in which case only that home is checked. */ + + if (!m->event) + return 0; + + if (IN_SET(sd_event_get_state(m->event), SD_EVENT_FINISHED, SD_EVENT_EXITING)) + return 0; + + /* If a focus home is specified, then remember to focus just on this home. Otherwise invalidate any + * focus that might be set to look at all homes. */ + + if (m->deferred_gc_event_source) { + if (m->gc_focus != focus) /* not the same focus, then look at everything */ + m->gc_focus = NULL; + + return 0; + } else + m->gc_focus = focus; /* start focussed */ + + r = sd_event_add_defer(m->event, &m->deferred_gc_event_source, on_deferred_gc, m); + if (r < 0) + return log_error_errno(r, "Failed to allocate gc event source: %m"); + + r = sd_event_source_set_priority(m->deferred_gc_event_source, SD_EVENT_PRIORITY_IDLE); + if (r < 0) + log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m"); + + (void) sd_event_source_set_description(m->deferred_gc_event_source, "deferred-gc"); + return 1; +} diff --git a/src/home/homed-manager.h b/src/home/homed-manager.h new file mode 100644 index 0000000000000..00298a3d2dcbc --- /dev/null +++ b/src/home/homed-manager.h @@ -0,0 +1,67 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +#include "sd-bus.h" +#include "sd-device.h" +#include "sd-event.h" + +typedef struct Manager Manager; + +#include "hashmap.h" +#include "homed-home.h" +#include "varlink.h" + +#define HOME_UID_MIN 60001 +#define HOME_UID_MAX 60513 + +struct Manager { + sd_event *event; + sd_bus *bus; + + Hashmap *polkit_registry; + + Hashmap *homes_by_uid; + Hashmap *homes_by_name; + Hashmap *homes_by_worker_pid; + Hashmap *homes_by_sysfs; + + bool scan_slash_home; + + sd_event_source *inotify_event_source; + + /* An even source we receieve sd_notify() messages from our worker from */ + sd_event_source *notify_socket_event_source; + + sd_device_monitor *device_monitor; + + sd_event_source *deferred_rescan_event_source; + sd_event_source *deferred_gc_event_source; + sd_event_source *deferred_auto_login_event_source; + + Home *gc_focus; + + VarlinkServer *varlink_server; + + EVP_PKEY *private_key; /* actually a pair of private and public key */ + Hashmap *public_keys; /* key name [char*] → publick key [EVP_PKEY*] */ +}; + +int manager_new(Manager **ret); +Manager* manager_free(Manager *m); +DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free); + +int manager_startup(Manager *m); + +int manager_augment_record_with_uid(Manager *m, UserRecord *hr); + +int manager_enqueue_rescan(Manager *m); +int manager_enqueue_gc(Manager *m, Home *focus); + +int manager_verify_user_record(Manager *m, UserRecord *hr); + +int manager_acquire_key_pair(Manager *m); +int manager_sign_user_record(Manager *m, UserRecord *u, UserRecord **ret, sd_bus_error *error); + +int bus_manager_emit_auto_login_changed(Manager *m); diff --git a/src/home/homed-operation.c b/src/home/homed-operation.c new file mode 100644 index 0000000000000..80dc555cd0e66 --- /dev/null +++ b/src/home/homed-operation.c @@ -0,0 +1,76 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "fd-util.h" +#include "homed-operation.h" + +Operation *operation_new(OperationType type, sd_bus_message *m) { + Operation *o; + + assert(type >= 0); + assert(type < _OPERATION_MAX); + + o = new(Operation, 1); + if (!o) + return NULL; + + *o = (Operation) { + .type = type, + .n_ref = 1, + .message = sd_bus_message_ref(m), + .send_fd = -1, + .result = -1, + }; + + return o; +} + +static Operation *operation_free(Operation *o) { + int r; + + if (!o) + return NULL; + + if (o->message && o->result >= 0) { + + if (o->result) { + /* Propagate success */ + if (o->send_fd < 0) + r = sd_bus_reply_method_return(o->message, NULL); + else + r = sd_bus_reply_method_return(o->message, "h", o->send_fd); + + } else { + /* Propagate failure */ + if (sd_bus_error_is_set(&o->error)) + r = sd_bus_reply_method_error(o->message, &o->error); + else + r = sd_bus_reply_method_errnof(o->message, o->ret, "Failed to execute operation: %m"); + } + if (r < 0) + log_warning_errno(r, "Failed ot reply to %s method call, ignoring: %m", sd_bus_message_get_member(o->message)); + } + + sd_bus_message_unref(o->message); + user_record_unref(o->secret); + safe_close(o->send_fd); + sd_bus_error_free(&o->error); + + return mfree(o); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(Operation, operation, operation_free); + +void operation_result(Operation *o, int ret, const sd_bus_error *error) { + assert(o); + + if (ret >= 0) + o->result = true; + else { + o->ret = ret; + + sd_bus_error_free(&o->error); + sd_bus_error_copy(&o->error, error); + + o->result = false; + } +} diff --git a/src/home/homed-operation.h b/src/home/homed-operation.h new file mode 100644 index 0000000000000..224de9185253e --- /dev/null +++ b/src/home/homed-operation.h @@ -0,0 +1,62 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +#include "user-record.h" + +typedef enum OperationType { + OPERATION_ACQUIRE, /* enqueued on AcquireHome() */ + OPERATION_RELEASE, /* enqueued on ReleaseHome() */ + OPERATION_LOCK_ALL, /* enqueued on LockAllHomes() */ + OPERATION_PIPE_EOF, /* enqueued when we see EOF on the per-home reference pipes */ + OPERATION_DEACTIVATE_FORCE, /* enqueued on hard $HOME unplug */ + OPERATION_IMMEDIATE, /* this is never enqueued, it's just a marker we immediately started executing an operation without enqueuing anything first. */ + _OPERATION_MAX, + _OPERATION_INVALID = -1, +} OperationType; + +/* Encapsulates an operation on one or more home directories. This has two uses: + * + * 1) For queuing an operation when we need to execute one for some reason but there's already one being + * executed. + * + * 2) When executing an operation without enqueuing it first (OPERATION_IMMEDIATE) + * + * Note that a single operation object can encapsulate operations on multiple home directories. This is used + * for the LockAllHomes() operation, which is one operation but applies to all homes at once. In case the + * operation applies to multiple homes the reference counter is increased once for each, and thus the + * operation is fully completed only after it reached zero again. + * + * The object (optionally) contains a reference of the D-Bus message triggering the operation, which is + * replied to when the operation is fully completed, i.e. when n_ref reaches zero. + */ + +typedef struct Operation { + unsigned n_ref; + OperationType type; + sd_bus_message *message; + + UserRecord *secret; + int send_fd; /* pipe fd for AcquireHome() which is taken already when we start the operation */ + + int result; /* < 0 if not completed yet, == 0 on failure, > 0 on success */ + sd_bus_error error; + int ret; +} Operation; + +Operation *operation_new(OperationType type, sd_bus_message *m); +Operation *operation_ref(Operation *operation); +Operation *operation_unref(Operation *operation); + +DEFINE_TRIVIAL_CLEANUP_FUNC(Operation*, operation_unref); + +void operation_result(Operation *o, int ret, const sd_bus_error *error); + +static inline Operation* operation_result_unref(Operation *o, int ret, const sd_bus_error *error) { + if (!o) + return NULL; + + operation_result(o, ret, error); + return operation_unref(o); +} diff --git a/src/home/homed-varlink.c b/src/home/homed-varlink.c new file mode 100644 index 0000000000000..4ef0c716171d5 --- /dev/null +++ b/src/home/homed-varlink.c @@ -0,0 +1,368 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "group-record.h" +#include "homed-varlink.h" +#include "strv.h" +#include "user-record-util.h" +#include "user-record.h" +#include "user-util.h" +#include "format-util.h" + +typedef struct LookupParameters { + const char *user_name; + const char *group_name; + union { + uid_t uid; + gid_t gid; + }; + const char *service; +} LookupParameters; + +static bool client_is_trusted(Varlink *link, Home *h) { + uid_t peer_uid; + int r; + + assert(link); + assert(h); + + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) { + log_debug_errno(r, "Unable to query peer UID, ignoring: %m"); + return false; + } + + return peer_uid == 0 || peer_uid == h->uid; +} + +static int build_user_json(Home *h, bool trusted, JsonVariant **ret) { + _cleanup_(user_record_unrefp) UserRecord *augmented = NULL; + UserRecordLoadFlags flags; + int r; + + assert(h); + assert(ret); + + 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 = home_augment_status(h, flags, &augmented); + if (r < 0) + return r; + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(augmented->json)), + JSON_BUILD_PAIR("incomplete", JSON_BUILD_BOOLEAN(augmented->incomplete)))); +} + +static bool home_user_match_lookup_parameters(LookupParameters *p, Home *h) { + assert(p); + assert(h); + + if (p->user_name && !streq(p->user_name, h->user_name)) + return false; + + if (uid_is_valid(p->uid) && h->uid != p->uid) + return false; + + return true; +} + +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, + }; + Manager *m = userdata; + bool trusted; + Home *h; + 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.Home")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + trusted = client_is_trusted(link, h); + + if (uid_is_valid(p.uid)) + h = hashmap_get(m->homes_by_uid, UID_TO_PTR(p.uid)); + else if (p.user_name) + h = hashmap_get(m->homes_by_name, p.user_name); + else { + Iterator i; + + /* If neither UID nor name was specified, then dump all homes. Do so with varlink_notify() + * for all entries but the last, so that clients can stream the results, and easily process + * them piecemeal. */ + + HASHMAP_FOREACH(h, m->homes_by_name, i) { + + if (!home_user_match_lookup_parameters(&p, h)) + continue; + + if (v) { + /* An entry set from the previous iteration? Then send it now */ + r = varlink_notify(link, v); + if (r < 0) + return r; + + v = json_variant_unref(v); + } + + r = build_user_json(h, trusted, &v); + if (r < 0) + return r; + } + + if (!v) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_reply(link, v); + } + + if (!h) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + if (!home_user_match_lookup_parameters(&p, h)) + return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + r = build_user_json(h, trusted, &v); + if (r < 0) + return r; + + return varlink_reply(link, v); +} + +static int build_group_json(Home *h, JsonVariant **ret) { + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; + int r; + + assert(h); + assert(ret); + + g = group_record_new(); + if (!g) + return -ENOMEM; + + r = group_record_synthesize(g, h->record); + if (r < 0) + return r; + + assert(!FLAGS_SET(g->mask, USER_RECORD_SECRET)); + assert(!FLAGS_SET(g->mask, USER_RECORD_PRIVILEGED)); + + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(g->json)))); +} + +static bool home_group_match_lookup_parameters(LookupParameters *p, Home *h) { + assert(p); + assert(h); + + if (p->group_name && !streq(h->user_name, p->group_name)) + return false; + + if (gid_is_valid(p->gid) && h->uid != (uid_t) p->gid) + return false; + + return true; +} + +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, + }; + Manager *m = userdata; + Home *h; + 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.Home")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + if (gid_is_valid(p.gid)) + h = hashmap_get(m->homes_by_uid, UID_TO_PTR((uid_t) p.gid)); + else if (p.group_name) + h = hashmap_get(m->homes_by_name, p.group_name); + else { + Iterator i; + + HASHMAP_FOREACH(h, m->homes_by_name, i) { + + if (!home_group_match_lookup_parameters(&p, h)) + continue; + + if (v) { + r = varlink_notify(link, v); + if (r < 0) + return r; + + v = json_variant_unref(v); + } + + r = build_group_json(h, &v); + if (r < 0) + return r; + } + + if (!v) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_reply(link, v); + } + + if (!h) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + if (!home_group_match_lookup_parameters(&p, h)) + return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + r = build_group_json(h, &v); + if (r < 0) + return r; + + return varlink_reply(link, v); +} + +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 }, + {} + }; + + Manager *m = userdata; + LookupParameters p = {}; + Home *h; + 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.Home")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + if (p.user_name) { + const char *last = NULL; + char **i; + + h = hashmap_get(m->homes_by_name, p.user_name); + if (!h) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + if (p.group_name) { + if (!strv_contains(h->record->member_of, p.group_name)) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(h->user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(p.group_name)))); + } + + STRV_FOREACH(i, h->record->member_of) { + if (last) { + r = varlink_notifyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(h->user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last)))); + if (r < 0) + return r; + } + + last = *i; + } + + if (last) + return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(h->user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last)))); + + } else if (p.group_name) { + const char *last = NULL; + Iterator i; + + HASHMAP_FOREACH(h, m->homes_by_name, i) { + + if (!strv_contains(h->record->member_of, p.group_name)) + 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(p.group_name)))); + if (r < 0) + return r; + } + + last = h->user_name; + } + + if (last) + return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(p.group_name)))); + } else { + const char *last_user_name = NULL, *last_group_name = NULL; + Iterator i; + + HASHMAP_FOREACH(h, m->homes_by_name, i) { + char **j; + + STRV_FOREACH(j, h->record->member_of) { + + 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; + } + + last_user_name = h->user_name; + last_group_name = *j; + } + } + + if (last_user_name) { + 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.NoRecordFound", NULL); +} diff --git a/src/home/homed-varlink.h b/src/home/homed-varlink.h new file mode 100644 index 0000000000000..4454d2344221f --- /dev/null +++ b/src/home/homed-varlink.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "homed-manager.h" + +int vl_method_get_user_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata); +int vl_method_get_group_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata); +int vl_method_get_memberships(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata); diff --git a/src/home/homed.c b/src/home/homed.c new file mode 100644 index 0000000000000..cd445354a1e13 --- /dev/null +++ b/src/home/homed.c @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include + +#include "daemon-util.h" +#include "homed-manager.h" +#include "log.h" +#include "main-func.h" +#include "signal-util.h" + +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(); + + // FIXME + log_set_max_level(LOG_DEBUG); + + umask(0022); + + if (argc != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments."); + + if (setenv("SYSTEMD_BYPASS_USERDB", "io.systemd.Home", 1) < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set $SYSTEMD_BYPASS_USERDB: %m"); + + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, SIGTERM, SIGINT, -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/home/homework.c b/src/home/homework.c new file mode 100644 index 0000000000000..95b5d442002a1 --- /dev/null +++ b/src/home/homework.c @@ -0,0 +1,5017 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "blkid-util.h" +#include "blockdev-util.h" +#include "btrfs-util.h" +#include "chattr-util.h" +#include "chown-recursive.h" +#include "copy.h" +#include "crypt-util.h" +#include "dirent-util.h" +#include "dm-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "fs-util.h" +#include "fsck-util.h" +#include "hexdecoct.h" +#include "home-util.h" +#include "id128-util.h" +#include "io-util.h" +#include "json.h" +#include "log.h" +#include "loop-util.h" +#include "main-func.h" +#include "memory-util.h" +#include "missing_magic.h" +#include "missing_syscall.h" +#include "mkdir.h" +#include "mount-util.h" +#include "mountpoint-util.h" +#include "parse-util.h" +#include "path-util.h" +#include "process-util.h" +#include "random-util.h" +#include "resize-fs.h" +#include "rm-rf.h" +#include "stat-util.h" +#include "string-util.h" +#include "strv.h" +#include "tmpfile-util.h" +#include "umask-util.h" +#include "user-record-util.h" +#include "user-record.h" +#include "user-util.h" +#include "virt.h" +#include "xattr-util.h" + +/* Round down to the nearest 1K size. Note that Linux generally handles block devices with 512 blocks only, + * but actually doesn't accept uneven numbers in many cases. To avoid any confusion around this we'll + * strictly round disk sizes down to the next 1K boundary.*/ +#define DISK_SIZE_ROUND_DOWN(x) ((x) & ~UINT64_C(1023)) + +/* Make sure a bad password always results in a 3s delay, no matter what */ +#define BAD_PASSWORD_DELAY_USEC (3 * USEC_PER_SEC) + +static bool supported_fstype(const char *fstype) { + /* Limit the set of supported file systems a bit, as protection against little tested kernel file + * systems. Also, we only support the resize ioctls for these file systems. */ + return STR_IN_SET(fstype, "ext4", "btrfs", "xfs"); +} + +static const char *mount_options_for_fstype(const char *fstype) { + if (streq(fstype, "ext4")) + return "noquota,user_xattr"; + if (streq(fstype, "xfs")) + return "noquota"; + if (streq(fstype, "btrfs")) + return "noacl"; + return NULL; +} + +static int probe_file_system_by_fd( + int fd, + char **ret_fstype, + sd_id128_t *ret_uuid) { + + _cleanup_(blkid_free_probep) blkid_probe b = NULL; + _cleanup_free_ char *s = NULL; + const char *fstype = NULL, *uuid = NULL; + sd_id128_t id; + int r; + + assert(fd >= 0); + assert(ret_fstype); + assert(ret_uuid); + + b = blkid_new_probe(); + if (!b) + return -ENOMEM; + + errno = 0; + r = blkid_probe_set_device(b, fd, 0, 0); + if (r != 0) + return errno > 0 ? -errno : -ENOMEM; + + (void) blkid_probe_enable_superblocks(b, 1); + (void) blkid_probe_set_superblocks_flags(b, BLKID_SUBLKS_TYPE|BLKID_SUBLKS_UUID); + + errno = 0; + r = blkid_do_safeprobe(b); + if (IN_SET(r, -2, 1)) /* nothing found or ambiguous result */ + return -ENOPKG; + if (r != 0) + return errno > 0 ? -errno : -EIO; + + (void) blkid_probe_lookup_value(b, "TYPE", &fstype, NULL); + if (!fstype) + return -ENOPKG; + + (void) blkid_probe_lookup_value(b, "UUID", &uuid, NULL); + if (!uuid) + return -ENOPKG; + + r = sd_id128_from_string(uuid, &id); + if (r < 0) + return r; + + s = strdup(fstype); + if (!s) + return -ENOMEM; + + *ret_fstype = TAKE_PTR(s); + *ret_uuid = id; + + return 0; +} + +static int probe_file_system_by_path(const char *path, char **ret_fstype, sd_id128_t *ret_uuid) { + _cleanup_close_ int fd = -1; + + fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (fd < 0) + return -errno; + + return probe_file_system_by_fd(fd, ret_fstype, ret_uuid); +} + +static int block_get_size_by_fd(int fd, uint64_t *ret) { + struct stat st; + + assert(fd >= 0); + assert(ret); + + if (fstat(fd, &st) < 0) + return -errno; + + if (!S_ISBLK(st.st_mode)) + return -ENOTBLK; + + if (ioctl(fd, BLKGETSIZE64, ret) < 0) + return -errno; + + return 0; +} + +static int block_get_size_by_path(const char *path, uint64_t *ret) { + _cleanup_close_ int fd = -1; + + fd = open(path, O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (fd < 0) + return -errno; + + return block_get_size_by_fd(fd, ret); +} + +static int run_fsck(const char *node, const char *fstype) { + int r, exit_status; + pid_t fsck_pid; + + assert(node); + assert(fstype); + + r = fsck_exists(fstype); + if (r < 0) + return log_error_errno(r, "Failed to check if fsck for file system %s exists: %m", fstype); + if (r == 0) { + log_warning("No fsck for file system %s installed, ignoring.", fstype); + return 0; + } + + r = safe_fork("(fsck)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_STDOUT_TO_STDERR, &fsck_pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + execl("/sbin/fsck", "/sbin/fsck", "-aTl", node, NULL); + log_error_errno(errno, "Failed to execute fsck: %m"); + _exit(FSCK_OPERATIONAL_ERROR); + } + + exit_status = wait_for_terminate_and_check("fsck", fsck_pid, WAIT_LOG_ABNORMAL); + if (exit_status < 0) + return exit_status; + if ((exit_status & ~FSCK_ERROR_CORRECTED) != 0) { + log_warning("fsck failed with exit status %i.", exit_status); + + if ((exit_status & (FSCK_SYSTEM_SHOULD_REBOOT|FSCK_ERRORS_LEFT_UNCORRECTED)) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "File system is corrupted, refusing."); + + log_warning("Ignoring fsck error."); + } + + log_info("File system check completed."); + + return 1; +} + +static int luks_setup( + const char *node, + const char *dm_name, + sd_id128_t uuid, + const char *cipher, + const char *cipher_mode, + uint64_t volume_key_size, + char **passwords, + bool discard, + struct crypt_device **ret, + sd_id128_t *ret_found_uuid, + void **ret_volume_key, + size_t *ret_volume_key_size) { + + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_(erase_and_freep) void *vk = NULL; + sd_id128_t p; + size_t ks; + char **pp; + int r; + + assert(node); + assert(dm_name); + assert(ret); + + r = crypt_init(&cd, node); + if (r < 0) + return log_error_errno(r, "Failed to allocate libcryptsetup context: %m"); + + crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); + + r = crypt_load(cd, CRYPT_LUKS2, NULL); + if (r < 0) + return log_error_errno(r, "Failed to load LUKS superblock: %m"); + + r = crypt_get_volume_key_size(cd); + if (r <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size"); + ks = (size_t) r; + + if (!sd_id128_is_null(uuid) || ret_found_uuid) { + const char *s; + + s = crypt_get_uuid(cd); + if (!s) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has no UUID."); + + r = sd_id128_from_string(s, &p); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has invalid UUID."); + + /* Check that the UUID matches, if specified */ + if (!sd_id128_is_null(uuid) && + !sd_id128_equal(uuid, p)) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has wrong UUID."); + } + + if (cipher && !streq_ptr(cipher, crypt_get_cipher(cd))) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock declares wrong cipher."); + + if (cipher_mode && !streq_ptr(cipher_mode, crypt_get_cipher_mode(cd))) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock declares wrong cipher mode."); + + if (volume_key_size != UINT64_MAX && ks != volume_key_size) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock declares wrong volume key size."); + + vk = malloc(ks); + if (!vk) + return log_oom(); + + STRV_FOREACH(pp, passwords) { + size_t vks = ks; + + r = crypt_volume_key_get( + cd, + CRYPT_ANY_SLOT, + vk, + &vks, + *pp, + strlen(*pp)); + if (r < 0) { + log_debug_errno(r, "Password %zu didn't work: %m", (size_t) (pp - passwords)); + continue; + } + + r = crypt_activate_by_volume_key( + cd, + dm_name, + vk, vks, + discard ? CRYPT_ACTIVATE_ALLOW_DISCARDS : 0); + if (r < 0) + return log_error_errno(r, "Failed to unlock LUKS superblock: %m"); + + log_info("Setting up LUKS device /dev/mapper/%s completed.", dm_name); + + *ret = TAKE_PTR(cd); + + if (ret_found_uuid) /* Return the UUID actually found if the caller wants to know */ + *ret_found_uuid = p; + if (ret_volume_key) + *ret_volume_key = TAKE_PTR(vk); + if (ret_volume_key_size) + *ret_volume_key_size = vks; + + return 0; + } + + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "No valid key for LUKS superblock."); +} + +static int luks_open( + const char *dm_name, + char **passwords, + struct crypt_device **ret, + sd_id128_t *ret_found_uuid, + void **ret_volume_key, + size_t *ret_volume_key_size) { + + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_(erase_and_freep) void *vk = NULL; + sd_id128_t p; + size_t ks; + char **pp; + int r; + + assert(dm_name); + assert(ret); + + /* Opens a LUKS device that is already set up. Re-validates the password while doing so (which also + * provides us with the volume key, which we want). */ + + r = crypt_init_by_name(&cd, dm_name); + if (r < 0) + return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name); + + crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); + + r = crypt_load(cd, CRYPT_LUKS2, NULL); + if (r < 0) + return log_error_errno(r, "Failed to load LUKS superblock: %m"); + + r = crypt_get_volume_key_size(cd); + if (r <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine LUKS volume key size"); + ks = (size_t) r; + + if (ret_found_uuid) { + const char *s; + + s = crypt_get_uuid(cd); + if (!s) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has no UUID."); + + r = sd_id128_from_string(s, &p); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "LUKS superblock has invalid UUID."); + } + + vk = malloc(ks); + if (!vk) + return log_oom(); + + STRV_FOREACH(pp, passwords) { + size_t vks = ks; + + r = crypt_volume_key_get( + cd, + CRYPT_ANY_SLOT, + vk, + &vks, + *pp, + strlen(*pp)); + if (r < 0) { + log_debug_errno(r, "Password %zu didn't work: %m", (size_t) (pp - passwords)); + continue; + } + + log_info("Discovered used LUKS device /dev/mapper/%s, and validated password.", dm_name); + + /* This is needed so that crypt_resize() can operate correctly for pre-existing LUKS + * devices. We need to tell libcryptsetup the volume key explicitly, so that it is in the + * kernel keyring. */ + r = crypt_activate_by_volume_key(cd, NULL, vk, vks, CRYPT_ACTIVATE_KEYRING_KEY); + if (r < 0) + return log_error_errno(r, "Failed to upload volume key again: %m"); + + log_info("Successfully re-activated LUKS device."); + + *ret = TAKE_PTR(cd); + + if (ret_found_uuid) + *ret_found_uuid = p; + if (ret_volume_key) + *ret_volume_key = TAKE_PTR(vk); + if (ret_volume_key_size) + *ret_volume_key_size = ks; + + return 0; + } + + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "No valid key for LUKS superblock."); +} + +static int fs_validate( + const char *dm_node, + sd_id128_t uuid, + char **ret_fstype, + sd_id128_t *ret_found_uuid) { + + _cleanup_free_ char *fstype = NULL; + sd_id128_t u; + int r; + + assert(dm_node); + assert(ret_fstype); + + r = probe_file_system_by_path(dm_node, &fstype, &u); + if (r < 0) + return log_error_errno(r, "Failed to probe file system: %m"); + + /* Limit the set of supported file systems a bit, as protection against little tested kernel file + * systems. Also, we only support the resize ioctls for these file systems. */ + if (!supported_fstype(fstype)) + return log_error_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "Image contains unsupported file system: %s", strna(fstype)); + + if (!sd_id128_is_null(uuid) && + !sd_id128_equal(uuid, u)) + return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "File system has wrong UUID."); + + log_info("Probing file system completed (found %s).", fstype); + + *ret_fstype = TAKE_PTR(fstype); + + if (ret_found_uuid) /* Return the UUID actually found if the caller wants to know */ + *ret_found_uuid = u; + + return 0; +} + +static int mount_node(const char *node, const char *fstype, bool discard) { + _cleanup_free_ char *joined = NULL; + const char *options, *discard_option; + int r; + + options = mount_options_for_fstype(fstype); + + discard_option = discard ? "discard" : "nodiscard"; + + if (options) { + joined = strjoin(options, ",", discard_option); + if (!joined) + return log_oom(); + + options = joined; + } else + options = discard_option; + + r = mount_verbose(LOG_ERR, node, "/run/systemd/user-home-mount", fstype, MS_NODEV|MS_NOSUID|MS_RELATIME, strempty(options)); + if (r < 0) + return r; + + log_info("Mounting file system completed."); + return 0; +} + +static int unshare_and_mount(const char *node, const char *fstype, bool discard) { + int r; + + if (unshare(CLONE_NEWNS) < 0) + return log_error_errno(errno, "Couldn't unshare file system namespace: %m"); + + r = mount_verbose(LOG_ERR, "/run", "/run", NULL, MS_SLAVE|MS_REC, NULL); /* Mark /run as MS_SLAVE in our new namespace */ + if (r < 0) + return r; + + (void) mkdir_p("/run/systemd/user-home-mount", 0700); + + if (node) + return mount_node(node, fstype, discard); + + return 0; +} + +typedef struct HomeSetup { + char *dm_name; + char *dm_node; + + LoopDevice *loop; + struct crypt_device *crypt_device; + int root_fd; + sd_id128_t found_partition_uuid; + sd_id128_t found_luks_uuid; + sd_id128_t found_fs_uuid; + + void *found_fscrypt_salt; + size_t found_fscrypt_salt_size; + + void *volume_key; + size_t volume_key_size; + + bool undo_dm; + bool undo_mount; + + uint64_t partition_offset; + uint64_t partition_size; +} HomeSetup; + +#define HOME_SETUP_INIT \ + { \ + .root_fd = -1, \ + .partition_offset = UINT64_MAX, \ + .partition_size = UINT64_MAX, \ + } + +static int home_setup_undo(HomeSetup *setup) { + int r = 0, q; + + assert(setup); + + setup->root_fd = safe_close(setup->root_fd); + + if (setup->undo_mount) { + q = umount_verbose("/run/systemd/user-home-mount"); + if (q < 0) + r = q; + } + + if (setup->undo_dm && setup->crypt_device && setup->dm_name) { + q = crypt_deactivate(setup->crypt_device, setup->dm_name); + if (q < 0) + r = q; + } + + setup->undo_mount = false; + setup->undo_dm = false; + + setup->dm_name = mfree(setup->dm_name); + setup->dm_node = mfree(setup->dm_node); + + setup->loop = loop_device_unref(setup->loop); + crypt_free(setup->crypt_device); + setup->crypt_device = NULL; + + explicit_bzero_safe(setup->volume_key, setup->volume_key_size); + setup->volume_key = mfree(setup->volume_key); + setup->volume_key_size = 0; + + setup->found_fscrypt_salt = mfree(setup->found_fscrypt_salt); + setup->found_fscrypt_salt_size = 0; + + return r; +} + +static int make_dm_names(const char *user_name, char **ret_dm_name, char **ret_dm_node) { + _cleanup_free_ char *name = NULL, *node = NULL; + + assert(user_name); + assert(ret_dm_name); + assert(ret_dm_node); + + name = strjoin("home-", user_name); + if (!name) + return log_oom(); + + node = path_join("/dev/mapper/", name); + if (!node) + return log_oom(); + + *ret_dm_name = TAKE_PTR(name); + *ret_dm_node = TAKE_PTR(node); + return 0; +} + +static int luks_validate( + int fd, + const char *label, + sd_id128_t partition_uuid, + sd_id128_t *ret_partition_uuid, + uint64_t *ret_offset, + uint64_t *ret_size) { + + _cleanup_(blkid_free_probep) blkid_probe b = NULL; + sd_id128_t found_partition_uuid = SD_ID128_NULL; + const char *fstype = NULL, *pttype = NULL; + blkid_loff_t offset = 0, size = 0; + blkid_partlist pl; + bool found = false; + int r, i, n; + + assert(fd >= 0); + assert(label); + assert(ret_offset); + assert(ret_size); + + b = blkid_new_probe(); + if (!b) + return -ENOMEM; + + errno = 0; + r = blkid_probe_set_device(b, fd, 0, 0); + if (r != 0) + return errno > 0 ? -errno : -ENOMEM; + + (void) blkid_probe_enable_superblocks(b, 1); + (void) blkid_probe_set_superblocks_flags(b, BLKID_SUBLKS_TYPE); + (void) blkid_probe_enable_partitions(b, 1); + (void) blkid_probe_set_partitions_flags(b, BLKID_PARTS_ENTRY_DETAILS); + + errno = 0; + r = blkid_do_safeprobe(b); + if (IN_SET(r, -2, 1)) /* nothing found or ambiguous result */ + return -ENOPKG; + if (r != 0) + return errno > 0 ? -errno : -EIO; + + (void) blkid_probe_lookup_value(b, "TYPE", &fstype, NULL); + if (streq_ptr(fstype, "crypto_LUKS")) { + /* Directly a LUKS image */ + *ret_offset = 0; + *ret_size = UINT64_MAX; /* full disk */ + *ret_partition_uuid = SD_ID128_NULL; + return 0; + } else if (fstype) + return -ENOPKG; + + (void) blkid_probe_lookup_value(b, "PTTYPE", &pttype, NULL); + if (!streq_ptr(pttype, "gpt")) + return -ENOPKG; + + errno = 0; + pl = blkid_probe_get_partitions(b); + if (!pl) + return errno > 0 ? -errno : -ENOMEM; + + errno = 0; + n = blkid_partlist_numof_partitions(pl); + if (n < 0) + return errno > 0 ? -errno : -EIO; + + for (i = 0; i < n; i++) { + blkid_partition pp; + sd_id128_t id; + const char *sid; + + errno = 0; + pp = blkid_partlist_get_partition(pl, i); + if (!pp) + return errno > 0 ? -errno : -EIO; + + if (!streq_ptr(blkid_partition_get_type_string(pp), "773f91ef-66d4-49b5-bd83-d683bf40ad16")) + continue; + + if (!streq_ptr(blkid_partition_get_name(pp), label)) + continue; + + sid = blkid_partition_get_uuid(pp); + if (sid) { + r = sd_id128_from_string(sid, &id); + if (r < 0) + log_debug_errno(r, "Couldn't parse partition UUID %s, weird: %m", sid); + + if (!sd_id128_is_null(partition_uuid) && !sd_id128_equal(id, partition_uuid)) + continue; + } + + if (found) + return -ENOPKG; + + offset = blkid_partition_get_start(pp); + size = blkid_partition_get_size(pp); + found_partition_uuid = id; + + found = true; + } + + if (!found) + return -ENOPKG; + + if (offset < 0) + return -EINVAL; + if ((uint64_t) offset > UINT64_MAX / 512U) + return -EINVAL; + if (size <= 0) + return -EINVAL; + if ((uint64_t) size > UINT64_MAX / 512U) + return -EINVAL; + + *ret_offset = offset * 512U; + *ret_size = size * 512U; + *ret_partition_uuid = found_partition_uuid; + + return 0; +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_CIPHER_CTX*, EVP_CIPHER_CTX_free); + +static int crypt_device_to_evp_cipher(struct crypt_device *cd, const EVP_CIPHER **ret) { + _cleanup_free_ char *cipher_name = NULL; + const char *cipher, *cipher_mode, *e; + size_t key_size, key_bits; + const EVP_CIPHER *cc; + int r; + + assert(cd); + + /* Let's find the right OpenSSL EVP_CIPHER object that matches the encryption settings of the LUKS + * device */ + + cipher = crypt_get_cipher(cd); + if (!cipher) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot get cipher from LUKS device."); + + cipher_mode = crypt_get_cipher_mode(cd); + if (!cipher_mode) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot get cipher mode from LUKS device."); + + e = strchr(cipher_mode, '-'); + if (e) + cipher_mode = strndupa(cipher_mode, e - cipher_mode); + + r = crypt_get_volume_key_size(cd); + if (r <= 0) + return log_error_errno(r < 0 ? r : SYNTHETIC_ERRNO(EINVAL), "Cannot get volume key size from LUKS device."); + + key_size = r; + key_bits = key_size * 8; + if (streq(cipher_mode, "xts")) + key_bits /= 2; + + if (asprintf(&cipher_name, "%s-%zu-%s", cipher, key_bits, cipher_mode) < 0) + return log_oom(); + + cc = EVP_get_cipherbyname(cipher_name); + if (!cc) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Selected cipher mode '%s' not supported, can't encrypt JSON record.", cipher_name); + + /* Verify that our key length calculations match what OpenSSL thinks */ + r = EVP_CIPHER_key_length(cc); + if (r < 0 || (uint64_t) r != key_size) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Key size of selected cipher doesn't meet out expectations."); + + *ret = cc; + return 0; +} + +static int luks_validate_home_record( + struct crypt_device *cd, + UserRecord *h, + const void *volume_key, + UserRecord **ret_luks_home_record) { + + int r, token; + + assert(cd); + assert(h); + + for (token = 0;; token++) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *rr = NULL; + _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL; + _cleanup_(user_record_unrefp) UserRecord *lhr = NULL; + _cleanup_free_ void *encrypted = NULL, *iv = NULL; + size_t decrypted_size, encrypted_size, iv_size; + int decrypted_size_out1, decrypted_size_out2; + _cleanup_free_ char *decrypted = NULL; + const char *text, *type; + crypt_token_info state; + JsonVariant *jr, *jiv; + unsigned line, column; + const EVP_CIPHER *cc; + + state = crypt_token_status(cd, token, &type); + if (state == CRYPT_TOKEN_INACTIVE) /* First unconfigured token, give up */ + break; + if (IN_SET(state, CRYPT_TOKEN_INTERNAL, CRYPT_TOKEN_INTERNAL_UNKNOWN, CRYPT_TOKEN_EXTERNAL)) + continue; + if (state != CRYPT_TOKEN_EXTERNAL_UNKNOWN) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unexpected token state of token %i: %i", token, (int) state); + + if (!streq(type, "systemd-homed")) + continue; + + r = crypt_token_json_get(cd, token, &text); + if (r < 0) + return log_error_errno(r, "Failed to read LUKS token %i: %m", token); + + r = json_parse(text, JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse LUKS token JSON data %u:%u: %m", line, column); + + jr = json_variant_by_key(v, "record"); + if (!jr) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "LUKS token lacks 'record' field."); + jiv = json_variant_by_key(v, "iv"); + if (!jiv) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "LUKS token lacks 'iv' field."); + + r = json_variant_unbase64(jr, &encrypted, &encrypted_size); + if (r < 0) + return log_error_errno(r, "Failed to base64 decode record: %m"); + + r = json_variant_unbase64(jiv, &iv, &iv_size); + if (r < 0) + return log_error_errno(r, "Failed to base64 decode IV: %m"); + + r = crypt_device_to_evp_cipher(cd, &cc); + if (r < 0) + return r; + if (iv_size > INT_MAX || EVP_CIPHER_iv_length(cc) != (int) iv_size) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "IV size doesn't match."); + + context = EVP_CIPHER_CTX_new(); + if (!context) + return log_oom(); + + if (EVP_DecryptInit_ex(context, cc, NULL, volume_key, iv) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize decryption context."); + + decrypted_size = encrypted_size + EVP_CIPHER_key_length(cc) * 2; + decrypted = new(char, decrypted_size); + if (!decrypted) + return log_oom(); + + if (EVP_DecryptUpdate(context, (uint8_t*) decrypted, &decrypted_size_out1, encrypted, encrypted_size) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to decrypt JSON record."); + + assert((size_t) decrypted_size_out1 <= decrypted_size); + + if (EVP_DecryptFinal_ex(context, (uint8_t*) decrypted + decrypted_size_out1, &decrypted_size_out2) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finish decryptio of JSON record."); + + assert((size_t) decrypted_size_out1 + (size_t) decrypted_size_out2 < decrypted_size); + decrypted_size = (size_t) decrypted_size_out1 + (size_t) decrypted_size_out2; + + if (memchr(decrypted, 0, decrypted_size)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Inner NUL byte in JSON record, refusing."); + + decrypted[decrypted_size] = 0; + + r = json_parse(decrypted, JSON_PARSE_SENSITIVE, &rr, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse decrypted JSON record, refusing."); + + lhr = user_record_new(); + if (!lhr) + return log_oom(); + + r = user_record_load(lhr, rr, USER_RECORD_LOAD_EMBEDDED); + if (r < 0) + return log_error_errno(r, "Failed to parse user record: %m"); + + if (!user_record_compatible(h, lhr)) + return log_error_errno(SYNTHETIC_ERRNO(EREMCHG), "LUKS home record not compatible with host record, refusing."); + + r = user_record_test_secret(lhr, h); + if (r == -ENXIO) + return log_error_errno(r, "Embedded LUKS user record contains no hashed password, invalid."); + if (r == -ENOKEY) + return log_error_errno(r, "Supplied password does not match password of embedded LUKS user record."); + if (r < 0) + return log_error_errno(r, "Failed to validate password of embedded LUKS user record: %m"); + + log_info("Provided password unlocks embedded LUKS user record."); + + *ret_luks_home_record = TAKE_PTR(lhr); + return 0; + } + + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Couldn't find home record in LUKS2 header, refusing."); +} + +static int format_luks_token_text( + struct crypt_device *cd, + UserRecord *hr, + const void *volume_key, + char **ret) { + + int r, encrypted_size_out1 = 0, encrypted_size_out2 = 0, iv_size, key_size; + _cleanup_(EVP_CIPHER_CTX_freep) EVP_CIPHER_CTX *context = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_free_ void *iv = NULL, *encrypted = NULL; + size_t text_length, encrypted_size; + _cleanup_free_ char *text = NULL; + const EVP_CIPHER *cc; + + assert(cd); + assert(hr); + assert(volume_key); + assert(ret); + + r = crypt_device_to_evp_cipher(cd, &cc); + if (r < 0) + return r; + + key_size = EVP_CIPHER_key_length(cc); + iv_size = EVP_CIPHER_iv_length(cc); + + if (iv_size > 0) { + iv = malloc(iv_size); + if (!iv) + return log_oom(); + + r = genuine_random_bytes(iv, iv_size, RANDOM_BLOCK); + if (r < 0) + return log_error_errno(r, "Failed to generate IV: %m"); + } + + context = EVP_CIPHER_CTX_new(); + if (!context) + return log_oom(); + + if (EVP_EncryptInit_ex(context, cc, NULL, volume_key, iv) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to initialize encryption context."); + + r = json_variant_format(hr->json, 0, &text); + if (r < 0) + return log_error_errno(r,"Failed to format user record for LUKS: %m"); + + text_length = strlen(text); + encrypted_size = text_length + 2*key_size - 1; + + encrypted = malloc(encrypted_size); + if (!encrypted) + return log_oom(); + + if (EVP_EncryptUpdate(context, encrypted, &encrypted_size_out1, (uint8_t*) text, text_length) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to encrypt JSON record."); + + assert((size_t) encrypted_size_out1 <= encrypted_size); + + if (EVP_EncryptFinal_ex(context, (uint8_t*) encrypted + encrypted_size_out1, &encrypted_size_out2) != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to finish encryption of JSON record. "); + + assert((size_t) encrypted_size_out1 + (size_t) encrypted_size_out2 <= encrypted_size); + + r = json_build(&v, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("type", JSON_BUILD_STRING("systemd-homed")), + JSON_BUILD_PAIR("keyslots", JSON_BUILD_EMPTY_ARRAY), + JSON_BUILD_PAIR("record", JSON_BUILD_BASE64(encrypted, encrypted_size_out1 + encrypted_size_out2)), + JSON_BUILD_PAIR("iv", JSON_BUILD_BASE64(iv, iv_size)))); + if (r < 0) + return log_error_errno(r, "Failed to prepare LUKS JSON token object: %m"); + + r = json_variant_format(v, 0, ret); + if (r < 0) + return log_error_errno(r, "Failed to format encrypted user record for LUKS: %m"); + + return 0; +} + +static int luks_store_header_identity( + UserRecord *h, + struct crypt_device *cd, + const void *volume_key, + UserRecord *old_home) { + + _cleanup_(user_record_unrefp) UserRecord *header_home = NULL; + _cleanup_free_ char *text = NULL; + int token = 0, r; + + assert(h); + + if (!cd) + return 0; + + assert(volume_key); + + /* Let's store the user's identity record in the LUKS2 "token" header data fields, in an encrypted + * fashion. Why that? If we'd rely on the record being embedded in the payload file system itself we + * would have to mount the file system before we can validate the JSON record, its signatures and + * whether it matches what we are looking for. However, kernel file system implementations are + * generally not ready to be used on untrusted media. Hence let's store the record independently of + * the file system, so that we can validate it first, and only then mount the file system. To keep + * things simple we use the same encryption settings for this record as for the file system itself. */ + + r = user_record_clone(h, USER_RECORD_EXTRACT_EMBEDDED, &header_home); + if (r < 0) + return log_error_errno(r, "Failed to determine new header record: %m"); + + if (old_home && user_record_equal(old_home, header_home)) { + log_debug("Not updating header home record."); + return 0; + } + + r = format_luks_token_text(cd, header_home, volume_key, &text); + if (r < 0) + return r; + + for (;; token++) { + crypt_token_info state; + const char *type; + + state = crypt_token_status(cd, token, &type); + if (state == CRYPT_TOKEN_INACTIVE) /* First unconfigured token, we are done */ + break; + if (IN_SET(state, CRYPT_TOKEN_INTERNAL, CRYPT_TOKEN_INTERNAL_UNKNOWN, CRYPT_TOKEN_EXTERNAL)) + continue; /* Not ours */ + if (state != CRYPT_TOKEN_EXTERNAL_UNKNOWN) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unexpected token state of token %i: %i", token, (int) state); + + if (!streq(type, "systemd-homed")) + continue; + + r = crypt_token_json_set(cd, token, text); + if (r < 0) + return log_error_errno(r, "Failed to set JSON token for slot %i: %m", token); + + /* Now, let's free the text so that for all further matching tokens we all crypt_json_token_set() + * with a NULL text in order to invalidate the tokens. */ + text = mfree(text); + token++; + } + + if (text) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Didn't find any record token to update."); + + log_info("Wrote LUKS header user record."); + + return 1; +} + +static int run_fitrim(int root_fd) { + char buf[FORMAT_BYTES_MAX]; + struct fstrim_range range = { + .len = UINT64_MAX, + }; + + /* If discarding is on, discard everything right after mounting, so that the discard setting takes + * effect on activation. */ + + assert(root_fd >= 0); + + if (ioctl(root_fd, FITRIM, &range) < 0) { + if (IN_SET(errno, ENOTTY, EOPNOTSUPP, EBADF)) { + log_debug_errno(errno, "File system does not support FITRIM, not trimming."); + return 0; + } + + return log_warning_errno(errno, "Failed to invoke FITRIM, ignoring: %m"); + } + + log_info("Discarded unused %s.", + format_bytes(buf, sizeof(buf), range.len)); + return 1; +} + +static int run_fallocate(int backing_fd, const struct stat *st) { + char buf[FORMAT_BYTES_MAX]; + + assert(backing_fd >= 0); + assert(st); + + /* If discarding is off, let's allocate the whole image before mounting, so that the setting takes + * effect on activation */ + + if (!S_ISREG(st->st_mode)) + return 0; + + if (st->st_blocks >= DIV_ROUND_UP(st->st_size, 512)) { + log_info("Backing file is fully allocated already."); + return 0; + } + + if (fallocate(backing_fd, FALLOC_FL_KEEP_SIZE, 0, st->st_size) < 0) { + + if (ERRNO_IS_NOT_SUPPORTED(errno)) { + log_debug_errno(errno, "fallocate() not supported on file system, ignoring."); + return 0; + } + + if (ERRNO_IS_DISK_SPACE(errno)) { + log_debug_errno(errno, "Not enough disk space to fully allocate home."); + return -ENOSPC; /* make recognizable */ + } + + return log_error_errno(errno, "Failed to allocate backing file blocks: %m"); + } + + log_info("Allocated additional %s.", + format_bytes(buf, sizeof(buf), (DIV_ROUND_UP(st->st_size, 512) - st->st_blocks) * 512)); + return 1; +} + +static int home_prepare_luks( + UserRecord *h, + bool already_activated, + const char *force_image_path, + HomeSetup *setup, + UserRecord **ret_luks_home) { + + sd_id128_t found_partition_uuid, found_luks_uuid, found_fs_uuid; + _cleanup_(user_record_unrefp) UserRecord *luks_home = NULL; + _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL; + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_(erase_and_freep) void *volume_key = NULL; + bool dm_activated = false, mounted = false; + _cleanup_close_ int root_fd = -1; + size_t volume_key_size = 0; + uint64_t offset, size; + int r; + + assert(h); + assert(setup); + assert(setup->dm_name); + assert(setup->dm_node); + + assert(user_record_storage(h) == USER_LUKS); + + if (already_activated) { + struct loop_info64 info; + const char *n; + + r = luks_open(setup->dm_name, h->password, &cd, &found_luks_uuid, &volume_key, &volume_key_size); + if (r < 0) + return r; + + r = luks_validate_home_record(cd, h, volume_key, &luks_home); + if (r < 0) + return r; + + n = crypt_get_device_name(cd); + if (!n) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine backing device for DM %s.", setup->dm_name); + + r = loop_device_open(n, O_RDWR, &loop); + if (r < 0) + return log_error_errno(r, "Failed to open loopback device %s: %m", n); + + if (ioctl(loop->fd, LOOP_GET_STATUS64, &info) < 0) { + _cleanup_free_ char *sysfs = NULL; + struct stat st; + + if (!IN_SET(errno, ENOTTY, EINVAL)) + return log_error_errno(errno, "Failed to get block device metrics of %s: %m", n); + + if (ioctl(loop->fd, BLKGETSIZE64, &size) < 0) + return log_error_errno(r, "Failed to read block device size of %s: %m", n); + + if (fstat(loop->fd, &st) < 0) + return log_error_errno(r, "Failed to stat block device %s: %m", n); + assert(S_ISBLK(st.st_mode)); + + if (asprintf(&sysfs, "/sys/dev/block/%u:%u/partition", major(st.st_rdev), minor(st.st_rdev)) < 0) + return log_oom(); + + if (access(sysfs, F_OK) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to determine whether %s exists: %m", sysfs); + + offset = 0; + } else { + _cleanup_free_ char *startfn = NULL, *buffer = NULL; + + if (asprintf(&sysfs, "/sys/dev/block/%u:%u/start", major(st.st_rdev), minor(st.st_rdev)) < 0) + return log_oom(); + + r = read_one_line_file(sysfs, &buffer); + if (r < 0) + return log_error_errno(r, "Failed to read partition start offset: %m"); + + r = safe_atou64(buffer, &offset); + if (r < 0) + return log_error_errno(r, "Failed to parse partition start offset: %m"); + + if (offset > UINT64_MAX / 512U) + return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Offset too large for 64 byte range, refusing."); + + offset *= 512U; + } + } else { + offset = info.lo_offset; + size = info.lo_sizelimit; + } + + found_partition_uuid = found_fs_uuid = SD_ID128_NULL; + + log_info("Discovered used loopback device %s.", loop->node); + + root_fd = open(user_record_home_directory(h), O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) { + r = log_error_errno(r, "Failed to open home directory: %m"); + goto fail; + } + } else { + _cleanup_free_ char *fstype = NULL, *subdir = NULL; + _cleanup_close_ int fd = -1; + const char *ip; + struct stat st; + + ip = force_image_path ?: user_record_image_path(h); + + subdir = path_join("/run/systemd/user-home-mount/", user_record_user_name_and_realm(h)); + if (!subdir) + return log_oom(); + + fd = open(ip, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (fd < 0) + return log_error_errno(errno, "Failed to open image file %s: %m", ip); + + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to fstat() image file: %m"); + if (!S_ISREG(st.st_mode) && !S_ISBLK(st.st_mode)) + return log_error_errno(errno, "Image file %s is not a regular file or block device: %m", ip); + + r = luks_validate(fd, user_record_user_name_and_realm(h), h->partition_uuid, &found_partition_uuid, &offset, &size); + if (r < 0) + return log_error_errno(r, "Failed to validate disk label: %m"); + + if (!user_record_luks_discard(h)) { + r = run_fallocate(fd, &st); + if (r < 0) + return r; + } + + r = loop_device_make_full(fd, O_RDWR, offset, size, 0, &loop); + if (r == -ENOENT) { + log_error_errno(r, "Loopback block device support is not available on this system."); + return -ENOLINK; /* make recognizable */ + } + if (r < 0) + return log_error_errno(r, "Failed to allocate loopback context: %m"); + + log_info("Setting up loopback device %s completed.", loop->node ?: ip); + + r = luks_setup(loop->node ?: ip, + setup->dm_name, + h->luks_uuid, + h->luks_cipher, + h->luks_cipher_mode, + h->luks_volume_key_size, + h->password, + user_record_luks_discard(h), + &cd, + &found_luks_uuid, + &volume_key, + &volume_key_size); + if (r < 0) + return r; + + dm_activated = true; + + r = luks_validate_home_record(cd, h, volume_key, &luks_home); + if (r < 0) + goto fail; + + r = fs_validate(setup->dm_node, h->file_system_uuid, &fstype, &found_fs_uuid); + if (r < 0) + goto fail; + + r = run_fsck(setup->dm_node, fstype); + if (r < 0) + goto fail; + + r = unshare_and_mount(setup->dm_node, fstype, user_record_luks_discard(h)); + if (r < 0) + goto fail; + + mounted = true; + + root_fd = open(subdir, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) { + r = log_error_errno(r, "Failed to open home directory: %m"); + goto fail; + } + + if (user_record_luks_discard(h)) + (void) run_fitrim(root_fd); + } + + setup->loop = TAKE_PTR(loop); + setup->crypt_device = TAKE_PTR(cd); + setup->root_fd = TAKE_FD(root_fd); + setup->found_partition_uuid = found_partition_uuid; + setup->found_luks_uuid = found_luks_uuid; + setup->found_fs_uuid = found_fs_uuid; + setup->partition_offset = offset; + setup->partition_size = size; + setup->volume_key = TAKE_PTR(volume_key); + setup->volume_key_size = volume_key_size; + + setup->undo_mount = mounted; + setup->undo_dm = dm_activated; + + if (ret_luks_home) + *ret_luks_home = TAKE_PTR(luks_home); + + return 0; + +fail: + if (mounted) + (void) umount_verbose("/run/systemd/user-home-mount"); + + if (dm_activated) + (void) crypt_deactivate(cd, setup->dm_name); + + return r; +} + +static int home_prepare_directory(UserRecord *h, bool already_activated, HomeSetup *setup) { + assert(h); + assert(setup); + + setup->root_fd = open(user_record_image_path(h), O_RDONLY|O_CLOEXEC|O_DIRECTORY); + if (setup->root_fd < 0) + return log_error_errno(errno, "Failed to open home directory: %m"); + + return 0; +} + +static int home_upload_fscrypt_key( + UserRecord *h, + const char *password, + const void *salt, + size_t salt_size, + const uint8_t match_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE], + uint8_t ret_key_descriptor[static FS_KEY_DESCRIPTOR_SIZE]) { + + uint8_t master[512 / 8] = {}, hashed[512 / 8] = {}, hashed2[512 / 8] = {}; + _cleanup_free_ char *hex = NULL; + const char *description; + struct fscrypt_key key; + key_serial_t serial; + int r; + + assert(h); + + /* Derive a master key from the password using PBKDF2-SHA512-HMAC */ + if (PKCS5_PBKDF2_HMAC(password, strlen(password), + salt, salt_size, + 0xFFFF, EVP_sha512(), sizeof(master), master) != 1) { + r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "PBKDF2 failed"); + goto finish; + } + + /* Derive the key descriptor from the master key, also in order to be compatible with e4crypt */ + assert_se(SHA512(master, sizeof(master), hashed) == hashed); + assert_se(SHA512(hashed, sizeof(hashed), hashed2) == hashed2); + + if (match_key_descriptor) { + assert_cc(FS_KEY_DESCRIPTOR_SIZE <= sizeof(hashed2)); + if (memcmp(match_key_descriptor, hashed2, FS_KEY_DESCRIPTOR_SIZE) != 0) + return -ENOANO; /* don't log here */ + } + + hex = hexmem(hashed2, 8); + if (!hex) { + r = log_oom(); + goto finish; + } + + description = strjoina("fscrypt:", hex); + + key = (struct fscrypt_key) { + .mode = 0, + .size = sizeof(master), + }; + + assert_cc(sizeof(master) <= sizeof(key.raw)); + memcpy(key.raw, master, sizeof(master)); + + /* Upload to the kernel */ + serial = add_key("logon", description, &key, sizeof(key), KEY_SPEC_THREAD_KEYRING); + if (serial < 0) { + r = log_error_errno(errno, "Failed to install master key in keyring: %m"); + goto finish; + } + + log_info("Configured encryption key."); + + if (ret_key_descriptor) { + assert_cc(FS_KEY_DESCRIPTOR_SIZE <= sizeof(hashed2)); + memcpy(ret_key_descriptor, hashed2, FS_KEY_DESCRIPTOR_SIZE); + } + + r = 0; + +finish: + explicit_bzero_safe(master, sizeof(master)); + explicit_bzero_safe(&key, sizeof(key)); + return r; +} + +static int home_prepare_fscrypt(UserRecord *h, bool already_activated, HomeSetup *setup) { + _cleanup_free_ char *salt_text = NULL; + struct fscrypt_policy policy; + bool uploaded = false; + const char *ip; + const void *salt = NULL; + _cleanup_free_ void *salt_buffer = NULL; + size_t salt_size = 0; + char **i; + int r; + + assert(h); + assert(setup); + assert(user_record_storage(h) == USER_FSCRYPT); + + assert_se(ip = user_record_image_path(h)); + + setup->root_fd = open(ip, O_RDONLY|O_CLOEXEC|O_DIRECTORY); + if (setup->root_fd < 0) + return log_error_errno(errno, "Failed to open home directory: %m"); + + r = fgetxattr_malloc(setup->root_fd, "trusted.fscrypt_salt", &salt_text); + if (r == -ENODATA) { + salt = h->fscrypt_salt; + salt_size = h->fscrypt_salt_size; + } else if (r < 0) + return log_error_errno(r, "Faile read fscrypt salt extended attribute of %s: %m", ip); + else if (r >= 0) { + r = unbase64mem_full(salt_text, r, true, &salt_buffer, &salt_size); + if (r < 0) + return log_error_errno(r, "fscrypt salt extended attribute on %s is not valid: %m", ip); + + if (h->fscrypt_salt_size > 0 && + memcmp_nn(h->fscrypt_salt, h->fscrypt_salt_size, salt_buffer, salt_size) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "fscrypt salt doesn't match record."); + + free_and_replace(setup->found_fscrypt_salt, salt_buffer); + setup->found_fscrypt_salt_size = salt_size; + salt = setup->found_fscrypt_salt; + } + + if (ioctl(setup->root_fd, FS_IOC_GET_ENCRYPTION_POLICY, &policy) < 0) { + if (errno == ENODATA) + return log_error_errno(errno, "Home directory %s is not encrypted.", ip); + if (ERRNO_IS_NOT_SUPPORTED(errno)) { + log_error_errno(errno, "File system does not support fscrypt: %m"); + return -ENOLINK; /* make recognizable */ + } + return log_error_errno(errno, "Failed to acquire encryption policy of %s: %m", ip); + } + + STRV_FOREACH(i, h->password) { + r = home_upload_fscrypt_key(h, *i, salt, salt_size, policy.master_key_descriptor, NULL); + if (r >= 0) { + uploaded = true; /* This is the key we are looking for and we uploaded it into the + * kernel */ + break; + } + if (r != -ENOANO) + return r; + + /* Try another password if we got ENOANO, i.e. when the password we supplied doesn't + * match the policy of the home directory. */ + } + + if (!uploaded) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Failed to set up home directory with provided passwords."); + + return 0; +} + +static int home_prepare_cifs( + UserRecord *h, + bool already_activated, + HomeSetup *setup) { + + char **pw; + int r; + + assert(h); + assert(setup); + assert(user_record_storage(h) == USER_CIFS); + + if (already_activated) + setup->root_fd = open(user_record_home_directory(h), O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + else { + bool mounted = false; + + r = unshare_and_mount(NULL, NULL, false); + if (r < 0) + return r; + + STRV_FOREACH(pw, h->password) { + _cleanup_(unlink_and_freep) char *p = NULL; + _cleanup_free_ char *options = NULL; + _cleanup_(fclosep) FILE *f = NULL; + pid_t mount_pid; + int exit_status; + + r = fopen_temporary(NULL, &f, &p); + if (r < 0) + return log_error_errno(r, "Failed to create temporary credentials file: %m"); + + fprintf(f, + "username=%s\n" + "password=%s\n", + user_record_cifs_user_name(h), + *pw); + + if (h->cifs_domain) + fprintf(f, "domain=%s\n", h->cifs_domain); + + r = fflush_and_check(f); + if (r < 0) + return log_error_errno(r, "Failed to write temporary credentials file: %m"); + + f = safe_fclose(f); + + if (asprintf(&options, "credentials=%s,uid=" UID_FMT ",forceuid,gid=" UID_FMT ",forcegid,file_mode=0%3o,dir_mode=0%3o", + p, h->uid, h->uid, h->access_mode, h->access_mode) < 0) + return log_oom(); + + r = safe_fork("(mount)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_STDOUT_TO_STDERR, &mount_pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + execl("/bin/mount", "/bin/mount", "-n", "-t", "cifs", + h->cifs_service, "/run/systemd/user-home-mount", + "-o", options, NULL); + + log_error_errno(errno, "Failed to execute fsck: %m"); + _exit(EXIT_FAILURE); + } + + exit_status = wait_for_terminate_and_check("mount", mount_pid, WAIT_LOG_ABNORMAL|WAIT_LOG_NON_ZERO_EXIT_STATUS); + if (exit_status < 0) + return exit_status; + if (exit_status != EXIT_SUCCESS) + return -EPROTO; + + mounted = true; + break; + } + + if (!mounted) + return log_error_errno(ENOKEY, "Failed to mount home directory with supplied password."); + + setup->root_fd = open("/run/systemd/user-home-mount", O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + } + if (setup->root_fd < 0) + return log_error_errno(r, "Failed to open home directory: %m"); + + return 0; +} + +static int home_prepare( + UserRecord *h, + bool already_activated, + HomeSetup *setup, + UserRecord **ret_header_home) { + + int r; + + assert(h); + assert(setup); + assert(!setup->loop); + assert(!setup->crypt_device); + assert(setup->root_fd < 0); + assert(!setup->undo_dm); + assert(!setup->undo_mount); + + /* Makes a home directory accessible (through the root_fd file descriptor, not by path!). */ + + switch (user_record_storage(h)) { + + case USER_LUKS: + return home_prepare_luks(h, already_activated, NULL, setup, ret_header_home); + + case USER_SUBVOLUME: + case USER_DIRECTORY: + r = home_prepare_directory(h, already_activated, setup); + break; + + case USER_FSCRYPT: + r = home_prepare_fscrypt(h, already_activated, setup); + break; + + case USER_CIFS: + r = home_prepare_cifs(h, already_activated, setup); + break; + + default: + return log_error_errno(SYNTHETIC_ERRNO(ENOLINK), "Processing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + } + + if (r < 0) + return r; + + if (ret_header_home) + *ret_header_home = NULL; + + return r; +} + +static int sync_and_statfs(int root_fd, struct statfs *ret) { + assert(root_fd >= 0); + + /* Let's sync this to disk, so that the disk space reported by fstatfs() below is accurate (for file + * systems such as btrfs where this is determined lazily). */ + + if (syncfs(root_fd) < 0) + return log_error_errno(errno, "Failed to synchronize file system: %m"); + + if (ret) + if (fstatfs(root_fd, ret) < 0) + return log_error_errno(errno, "Failed to statfs() file system: %m"); + + log_info("Synchronized disk."); + + return 0; +} + +static void print_size_summary(uint64_t host_size, uint64_t encrypted_size, struct statfs *sfs) { + char buffer1[FORMAT_BYTES_MAX], buffer2[FORMAT_BYTES_MAX], buffer3[FORMAT_BYTES_MAX], buffer4[FORMAT_BYTES_MAX]; + + assert(sfs); + + log_info("Image size is %s, file system size is %s, file system payload size is %s, file system free is %s.", + format_bytes(buffer1, sizeof(buffer1), host_size), + format_bytes(buffer2, sizeof(buffer2), encrypted_size), + format_bytes(buffer3, sizeof(buffer3), (uint64_t) sfs->f_blocks * (uint64_t) sfs->f_frsize), + format_bytes(buffer4, sizeof(buffer4), (uint64_t) sfs->f_bfree * (uint64_t) sfs->f_frsize)); +} + +static int read_identity_file(int root_fd, JsonVariant **ret) { + _cleanup_(fclosep) FILE *identity_file = NULL; + _cleanup_close_ int identity_fd = -1; + unsigned line, column; + int r; + + assert(root_fd >= 0); + assert(ret); + + identity_fd = openat(root_fd, ".identity", O_RDONLY|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW|O_NONBLOCK); + if (identity_fd < 0) + return log_error_errno(errno, "Failed to open .identity file in home directory: %m"); + + r = fd_verify_regular(identity_fd); + if (r < 0) + return log_error_errno(r, "Embedded identity file is not a regular file, refusing: %m"); + + identity_file = fdopen(identity_fd, "r"); + if (!identity_file) + return log_oom(); + + identity_fd = -1; + + r = json_parse_file(identity_file, ".identity", JSON_PARSE_SENSITIVE, ret, &line, &column); + if (r < 0) + return log_error_errno(r, "[.identity:%u:%u] Failed to parse JSON data: %m", line, column); + + log_info("Read embedded .identity file."); + + return 0; +} + +static int write_identity_file(int root_fd, JsonVariant *v, uid_t uid) { + _cleanup_(json_variant_unrefp) JsonVariant *normalized = NULL; + _cleanup_(fclosep) FILE *identity_file = NULL; + _cleanup_close_ int identity_fd = -1; + _cleanup_free_ char *fn = NULL; + int r; + + assert(root_fd >= 0); + assert(v); + + normalized = json_variant_ref(v); + + r = json_variant_normalize(&normalized); + if (r < 0) + log_warning_errno(r, "Failed to normalize user record, ignoring: %m"); + + r = tempfn_random(".identity", NULL, &fn); + if (r < 0) + return r; + + identity_fd = openat(root_fd, fn, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW, 0600); + if (identity_fd < 0) + return log_error_errno(errno, "Failed to create .identity file in home directory: %m"); + + identity_file = fdopen(identity_fd, "w"); + if (!identity_file) { + r = log_oom(); + goto fail; + } + + identity_fd = -1; + + json_variant_dump(normalized, JSON_FORMAT_PRETTY, identity_file, NULL); + + r = fflush_and_check(identity_file); + if (r < 0) { + log_error_errno(r, "Failed to write .identity file: %m"); + goto fail; + } + + if (fchown(fileno(identity_file), uid, uid) < 0) { + log_error_errno(r, "Failed to change ownership of identity file: %m"); + goto fail; + } + + if (renameat(root_fd, fn, root_fd, ".identity") < 0) { + r = log_error_errno(errno, "Failed to move identity file into place: %m"); + goto fail; + } + + log_info("Wrote embedded .identity file."); + + return 0; + +fail: + (void) unlinkat(root_fd, fn, 0); + return r; +} + +static int load_embedded_identity( + UserRecord *h, + int root_fd, + UserRecord *header_home, + UserReconcileMode mode, + UserRecord **ret_embedded_home, + UserRecord **ret_new_home) { + + _cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *intermediate_home = NULL, *new_home = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + int r; + + assert(h); + assert(root_fd >= 0); + + r = read_identity_file(root_fd, &v); + if (r < 0) + return r; + + embedded_home = user_record_new(); + if (!embedded_home) + return log_oom(); + + r = user_record_load(embedded_home, v, USER_RECORD_LOAD_EMBEDDED); + if (r < 0) + return r; + + /* Insist that credentials the user supplies alos unlocks any embedded records. */ + r = user_record_test_secret(embedded_home, h); + if (r == -ENXIO) + return log_error_errno(r, "Embedded home user record contains no hashed password, invalid."); + if (r == -ENOKEY) + return log_error_errno(r, "Supplied password does not match password of embedded home user record."); + if (r < 0) + return log_error_errno(r, "Failed to validate password of embedded home user record: %m"); + + /* At this point we have three records to deal with: + * + * · The record we got passed from the host + * · The record included in the LUKS header (only if LUKS is used) + * · The record in the home directory itself (~.identity) + * + * Now we have to reconcile all three, and let the newest one win. */ + + if (header_home) { + /* Note we relax the requirements here. Instead of insisting that the host record is strictly + * newer, let's also be OK if its equally new. If it is, we'll however insist that the + * embedded record must be newer, so that we update at least one of the two. */ + + r = user_record_reconcile(h, header_home, mode == USER_RECONCILE_REQUIRE_NEWER ? USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL : mode, &intermediate_home); + if (r == -EREMCHG) /* this was supposed to be checked earlier already, but let's check this again */ + return log_error_errno(r, "Identity stored on host and in header don't match, refusing."); + if (r == -ESTALE) + return log_error_errno(r, "Embedded identity record is newer than supplied record, refusing."); + if (r < 0) + return log_error_errno(r, "Failed to reconcile host and header identities: %m"); + if (r == USER_RECONCILE_EMBEDDED_WON) + log_info("Reconciling header user identity completed (header version was newer)."); + else if (r == USER_RECONCILE_HOST_WON) { + log_info("Reconciling header user identity completed (host version was newer)."); + + if (mode == USER_RECONCILE_REQUIRE_NEWER) /* Host version is newer than the header + * version, hence we'll update + * something. This means we can relax the + * requirements on the embedded + * identity. */ + mode = USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL; + } else { + assert(r == USER_RECONCILE_IDENTICAL); + log_info("Reconciling user identities completed (host and header version were identical)."); + } + + h = intermediate_home; + } + + r = user_record_reconcile(h, embedded_home, mode, &new_home); + if (r == -EREMCHG) + return log_error_errno(r, "Identity stored on host and in home don't match, refusing."); + if (r == -ESTALE) + return log_error_errno(r, "Embedded identity record is equally new or newer than supplied record, refusing."); + if (r < 0) + return log_error_errno(r, "Failed to reconcile host and embedded identities: %m"); + if (r == USER_RECONCILE_EMBEDDED_WON) + log_info("Reconciling embedded user identity completed (embedded version was newer)."); + else if (r == USER_RECONCILE_HOST_WON) + log_info("Reconciling embedded user identity completed (host version was newer)."); + else { + assert(r == USER_RECONCILE_IDENTICAL); + log_info("Reconciling embedded user identity completed (host and embedded version were identical)."); + } + + if (ret_embedded_home) + *ret_embedded_home = TAKE_PTR(embedded_home); + + if (ret_new_home) + *ret_new_home = TAKE_PTR(new_home); + + return 0; +} + +static int store_embedded_identity(UserRecord *h, int root_fd, uid_t uid, UserRecord *old_home) { + _cleanup_(user_record_unrefp) UserRecord *embedded = NULL; + int r; + + assert(h); + assert(root_fd >= 0); + assert(uid_is_valid(uid)); + + r = user_record_clone(h, USER_RECORD_EXTRACT_EMBEDDED, &embedded); + if (r < 0) + return log_error_errno(r, "Failed to determine new embedded record: %m"); + + if (old_home && user_record_equal(old_home, embedded)) { + log_debug("Not updating embedded home record."); + return 0; + } + + /* The identity has changed, let's update it in the image */ + r = write_identity_file(root_fd, embedded->json, h->uid); + if (r < 0) + return r; + + return 1; +} + +static uint64_t luks_volume_key_size_convert(struct crypt_device *cd) { + int k; + + assert(cd); + + k = crypt_get_volume_key_size(cd); + if (k <= 0) + return UINT64_MAX; + + return (uint64_t) k; +} + +static const char *file_system_type_fd(int fd) { + struct statfs sfs; + + assert(fd >= 0); + + if (fstatfs(fd, &sfs) < 0) { + log_debug_errno(errno, "Failed to statfs(): %m"); + return NULL; + } + + if (is_fs_type(&sfs, XFS_SB_MAGIC)) + return "xfs"; + if (is_fs_type(&sfs, EXT4_SUPER_MAGIC)) + return "ext4"; + if (is_fs_type(&sfs, BTRFS_SUPER_MAGIC)) + return "btrfs"; + + return 0; +} + +static int extend_embedded_identity(UserRecord *h, UserRecord *used, HomeSetup *setup) { + int r; + + assert(h); + assert(used); + assert(setup); + + r = user_record_add_binding( + h, + user_record_storage(used), + user_record_image_path(used), + setup->found_partition_uuid, + setup->found_luks_uuid, + setup->found_fs_uuid, + setup->crypt_device ? crypt_get_cipher(setup->crypt_device) : NULL, + setup->crypt_device ? crypt_get_cipher_mode(setup->crypt_device) : NULL, + setup->crypt_device ? luks_volume_key_size_convert(setup->crypt_device) : UINT64_MAX, + file_system_type_fd(setup->root_fd), + setup->found_fscrypt_salt, + setup->found_fscrypt_salt_size, + user_record_home_directory(used), + used->uid, + (gid_t) used->uid); + if (r < 0) + return log_error_errno(r, "Failed to update binding in record: %m"); + + return 0; +} + +static int chown_recursive_directory(int root_fd, uid_t uid) { + int r; + + assert(root_fd >= 0); + assert(uid_is_valid(uid)); + + r = fd_chown_recursive(root_fd, uid, (gid_t) uid, 0777); + if (r < 0) + return log_error_errno(r, "Failed to change ownership of files and directories: %m"); + if (r == 0) + log_info("Recursive changing of ownership not necessary, skipped."); + else + log_info("Recursive changing of ownership completed."); + + return 0; +} + +static int move_mount(const char *user_name_and_realm, const char *target) { + _cleanup_free_ char *subdir = NULL; + const char *d; + int r; + + assert(user_name_and_realm); + assert(target); + + if (user_name_and_realm) { + subdir = path_join("/run/systemd/user-home-mount/", user_name_and_realm); + if (!subdir) + return log_oom(); + + d = subdir; + } else + d = "/run/systemd/user-home-mount/"; + + (void) mkdir_p(target, 0700); + + r = mount_verbose(LOG_ERR, d, target, NULL, MS_BIND, NULL); + if (r < 0) + return r; + + r = umount_verbose("/run/systemd/user-home-mount"); + if (r < 0) + return r; + + log_info("Moving to final mount point %s completed.", target); + return 0; +} + +static int home_freshen( + UserRecord *h, + HomeSetup *setup, + UserRecord *header_home, + struct statfs *ret_statfs, + UserRecord **ret_new_home) { + + _cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *new_home = NULL; + int r; + + assert(h); + assert(setup); + assert(ret_new_home); + + /* When activating a home directory, does the identity work: loads the identity from the $HOME + * directory, reconciles it with our idea, chown()s everything. */ + + r = load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_ANY, &embedded_home, &new_home); + if (r < 0) + return r; + + r = luks_store_header_identity(new_home, setup->crypt_device, setup->volume_key, header_home); + if (r < 0) + return r; + + r = store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home); + if (r < 0) + return r; + + r = chown_recursive_directory(setup->root_fd, h->uid); + if (r < 0) + return r; + + r = sync_and_statfs(setup->root_fd, ret_statfs); + if (r < 0) + return r; + + *ret_new_home = TAKE_PTR(new_home); + return 0; +} + +static int home_activate_luks(UserRecord *h, UserRecord **ret_home) { + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *luks_home_record = NULL; + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + _cleanup_free_ char *fstype = NULL; + uint64_t host_size, encrypted_size; + const char *hdo, *hd; + struct statfs sfs; + int r; + + assert(h); + assert(user_record_storage(h) == USER_LUKS); + assert(ret_home); + + assert_se(hdo = user_record_home_directory(h)); + hd = strdupa(hdo); /* copy the string out, since it might change later in the home record object */ + + r = make_dm_names(h->user_name, &setup.dm_name, &setup.dm_node); + if (r < 0) + return r; + + r = access(setup.dm_node, F_OK); + if (r < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to determine whether %s exists: %m", setup.dm_node); + } else + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Device mapper device %s already exists, refusing.", setup.dm_node); + + r = home_prepare_luks(h, false, NULL, &setup, &luks_home_record); + if (r < 0) + return r; + + r = block_get_size_by_fd(setup.loop->fd, &host_size); + if (r < 0) + return log_error_errno(r, "Failed to get loopback block device size: %m"); + + r = block_get_size_by_path(setup.dm_node, &encrypted_size); + if (r < 0) + return log_error_errno(r, "Failed to get LUKS block device size: %m"); + + r = home_freshen(h, &setup, luks_home_record, &sfs, &new_home); + if (r < 0) + return r; + + r = extend_embedded_identity(new_home, h, &setup); + if (r < 0) + return r; + + setup.root_fd = safe_close(setup.root_fd); + + r = move_mount(user_record_user_name_and_realm(h), hd); + if (r < 0) + return r; + + setup.undo_mount = false; + + loop_device_relinquish(setup.loop); + + r = dm_deferred_remove(setup.dm_name); + if (r < 0) + log_warning_errno(r, "Failed to relinquish dm device, ignoring: %m"); + + setup.undo_dm = false; + + log_info("Everything completed."); + + print_size_summary(host_size, encrypted_size, &sfs); + + *ret_home = TAKE_PTR(new_home); + return 1; +} + +static int home_activate_directory(UserRecord *h, UserRecord **ret_home) { + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *header_home = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *embedded_json = NULL; + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + const char *hdo, *hd, *ipo, *ip; + int r; + + assert(h); + assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT)); + assert(ret_home); + + assert_se(ipo = user_record_image_path(h)); + ip = strdupa(ipo); /* copy out, since reconciliation might cause changing of the field */ + + assert_se(hdo = user_record_home_directory(h)); + hd = strdupa(hdo); + + r = home_prepare(h, false, &setup, &header_home); + if (r < 0) + return r; + + r = home_freshen(h, &setup, header_home, NULL, &new_home); + if (r < 0) + return r; + + setup.root_fd = safe_close(setup.root_fd); + + /* Create mount point to mount over if necessary */ + if (!path_equal(ip, hd)) + (void) mkdir_p(hd, 0700); + + /* Create a mount point (even if the directory is already placed correctly), as a way to indicate + * this mount point is now "activated". Moreover, we want to set per-user + * MS_NOSUID/MS_NOEXEC/MS_NODEV. */ + r = mount_verbose(LOG_ERR, ip, hd, NULL, MS_BIND, NULL); + if (r < 0) + return r; + + r = mount_verbose(LOG_ERR, NULL, hd, NULL, MS_BIND|MS_REMOUNT|user_record_mount_flags(h), NULL); + if (r < 0) { + (void) umount_verbose(hd); + return r; + } + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 0; +} + +static int home_activate_cifs(UserRecord *h, UserRecord **ret_home) { + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + _cleanup_free_ char *fstype = NULL; + const char *hdo, *hd; + int r; + + assert(h); + assert(user_record_storage(h) == USER_CIFS); + assert(ret_home); + + if (!h->cifs_service) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks CIFS service, refusing."); + + assert_se(hdo = user_record_home_directory(h)); + hd = strdupa(hdo); /* copy the string out, since it might change later in the home record object */ + + r = home_prepare_cifs(h, false, &setup); + if (r < 0) + return r; + + r = home_freshen(h, &setup, NULL, NULL, &new_home); + if (r < 0) + return r; + + setup.root_fd = safe_close(setup.root_fd); + + r = move_mount(NULL, hd); + if (r < 0) + return r; + + setup.undo_mount = false; + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 1; +} + +static int user_record_validate_hashed_password(UserRecord *h) { + int r; + + assert(h); + + if (strv_isempty(h->password)) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "No password provided."); + + r = user_record_test_secret(h, h); + if (r == -ENXIO) { + log_info("Supplied user record contains no hashed passwords, not validating password against it."); + return 0; + } + if (r == -ENOKEY) + return log_error_errno(r, "Passwords not correct or not sufficient to unlock record."); + if (r < 0) + return log_error_errno(r, "Failed to validate password of record: %m"); + + log_info("Provided password unlocks user record."); + + return 0; +} + +static int home_activate(UserRecord *h, UserRecord **ret_home) { + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + int r; + + assert(h); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks user name, refusing."); + if (!uid_is_valid(h->uid)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks UID, refusing."); + if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Activating home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + r = user_record_validate_hashed_password(h); + if (r < 0) + return r; + + r = user_record_test_home_directory_and_warn(h); + if (r < 0) + return r; + if (r == USER_TEST_MOUNTED) + return log_error_errno(SYNTHETIC_ERRNO(EALREADY), "Home directory %s is already mounted, refusing.", user_record_home_directory(h)); + + r = user_record_test_image_path_and_warn(h); + if (r < 0) + return r; + if (r == USER_TEST_ABSENT) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Image path %s is missing, refusing.", user_record_image_path(h)); + + switch (user_record_storage(h)) { + + case USER_LUKS: + r = home_activate_luks(h, &new_home); + if (r < 0) + return r; + + break; + + case USER_SUBVOLUME: + case USER_DIRECTORY: + case USER_FSCRYPT: + r = home_activate_directory(h, &new_home); + if (r < 0) + return r; + + break; + + case USER_CIFS: + r = home_activate_cifs(h, &new_home); + if (r < 0) + return r; + + break; + + default: + assert_not_reached("unexpected type"); + } + + /* Note that the returned object might either be a reference to an updated version of the existing + * home object, or a reference to a newly allocated home object. The caller has to be able to deal + * with both, and consider the old object out-of-date. */ + if (user_record_equal(h, new_home)) { + *ret_home = NULL; + return 0; /* no identity change */ + } + + *ret_home = TAKE_PTR(new_home); + return 1; /* identity updated */ +} + +static int home_deactivate(UserRecord *h, bool force) { + bool done = false; + int r; + + assert(h); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record incomplete, refusing."); + if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Deactivating home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + r = user_record_test_home_directory_and_warn(h); + if (r < 0) + return r; + if (r == USER_TEST_MOUNTED) { + if (umount2(user_record_home_directory(h), UMOUNT_NOFOLLOW | (force ? MNT_FORCE|MNT_DETACH : 0)) < 0) + return log_error_errno(errno, "Failed to unmount %s: %m", user_record_home_directory(h)); + + log_info("Unmounting completed."); + done = true; + } else + log_info("Directory %s is already unmounted.", user_record_home_directory(h)); + + if (user_record_storage(h) == USER_LUKS) { + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_free_ char *dm_name = NULL, *dm_node = NULL; + + /* Note that the DM device and loopback device are set to auto-detach, hence strictly + * speaking we don't have to explicitly have to detach them. However, we do that nonetheless + * (in case of the DM device), to avoid races: by explicitly detaching them we know when the + * detaching is complete. We don't bother about the loopback device because unlike the DM + * device it doesn't have a fixed name. */ + + r = make_dm_names(h->user_name, &dm_name, &dm_node); + if (r < 0) + return r; + + r = crypt_init_by_name(&cd, dm_name); + if (IN_SET(r, -ENODEV, -EINVAL, -ENOENT)) + log_debug_errno(r, "LUKS device %s is already detached.", dm_name); + else if (r < 0) + return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name); + else { + log_info("Discovered used LUKS device %s.", dm_node); + + crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); + + r = crypt_deactivate(cd, dm_name); + if (IN_SET(r, -ENODEV, -EINVAL, -ENOENT)) + log_debug_errno(r, "LUKS device %s is already detached.", dm_node); + else if (r < 0) + return log_info_errno(r, "LUKS device %s couldn't be deactivated: %m", dm_node); + + log_info("LUKS device detaching completed."); + done = true; + } + } + + if (!done) + return log_error_errno(SYNTHETIC_ERRNO(ENOEXEC), "Home is not active."); + + log_info("Everything completed."); + return 0; +} + +static int run_mkfs( + const char *node, + const char *fstype, + const char *label, + sd_id128_t uuid, + bool discard) { + + int r; + + assert(node); + assert(fstype); + assert(label); + + r = mkfs_exists(fstype); + if (r < 0) + return log_error_errno(r, "Failed to check if mkfs for file system %s exists: %m", fstype); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "Nt mkfs for file system %s installed.", fstype); + + r = safe_fork("(mkfs)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_WAIT|FORK_STDOUT_TO_STDERR, NULL); + if (r < 0) + return r; + if (r == 0) { + const char *mkfs; + char suuid[37]; + + /* Child */ + + mkfs = strjoina("mkfs.", fstype); + id128_to_uuid_string(uuid, suuid); + + if (streq(fstype, "ext4")) + execlp(mkfs, mkfs, + "-L", label, + "-U", suuid, + "-I", "256", + "-O", "has_journal", + "-m", "0", + "-E", discard ? "lazy_itable_init=1,discard" : "lazy_itable_init=1,nodiscard", + node, NULL); + else if (streq(fstype, "btrfs")) { + if (discard) + execlp(mkfs, mkfs, "-L", label, "-U", suuid, node, NULL); + else + execlp(mkfs, mkfs, "-L", label, "-U", suuid, "--nodiscard", node, NULL); + } else if (streq(fstype, "xfs")) { + const char *j; + + j = strjoina("uuid=", suuid); + if (discard) + execlp(mkfs, mkfs, "-L", label, "-m", j, "-m", "reflink=1", node, NULL); + else + execlp(mkfs, mkfs, "-L", label, "-m", j, "-m", "reflink=1", "-K", node, NULL); + } else { + log_error("Cannot make file system: %s", fstype); + _exit(EXIT_FAILURE); + } + + log_error_errno(errno, "Failed to execute %s: %m", mkfs); + _exit(EXIT_FAILURE); + } + + return 0; +} + +static int luks_format( + const char *node, + const char *dm_name, + sd_id128_t uuid, + const char *label, + char **passwords, + char **hashed_passwords, + bool discard, + UserRecord *hr, + struct crypt_device **ret) { + + _cleanup_(user_record_unrefp) UserRecord *reduced = NULL; + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_(erase_and_freep) void *volume_key = NULL; + _cleanup_free_ char *text = NULL; + size_t volume_key_size; + char suuid[37], **pp; + bool added = false; + int slot = 0, r; + + assert(node); + assert(dm_name); + assert(hr); + assert(ret); + + r = crypt_init(&cd, node); + if (r < 0) + return log_error_errno(r, "Failed to allocate libcryptsetup context: %m"); + + crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); + + /* Normally we'd, just leave volume key generation to libcryptsetup. However, we can't, since we + * can't extract the volume key from the library again, but we need it in order to encrypt the JSON + * record. Hence, let's generate it on our own, so that we can keep track of it. */ + + volume_key_size = user_record_luks_volume_key_size(hr); + volume_key = malloc(volume_key_size); + if (!volume_key) + return log_oom(); + + r = genuine_random_bytes(volume_key, volume_key_size, RANDOM_BLOCK); + if (r < 0) + return log_error_errno(r, "Failed to generate volume key: %m"); + + /* Increase the metadata space to 4M, the largest LUKS2 supports */ + r = crypt_set_metadata_size(cd, 4096U*1024U, 0); + if (r < 0) + return log_error_errno(r, "Failed to change LUKS2 metadata size: %m"); + + r = crypt_format(cd, + CRYPT_LUKS2, + user_record_luks_cipher(hr), + user_record_luks_cipher_mode(hr), + id128_to_uuid_string(uuid, suuid), + volume_key, + volume_key_size, + &(struct crypt_params_luks2) { + .label = label, + .subsystem = "systemd-home", + .sector_size = 512U, + .pbkdf = &(struct crypt_pbkdf_type) { + .hash = user_record_luks_pbkdf_hash_algorithm(hr), + .type = user_record_luks_pbkdf_type(hr), + .time_ms = user_record_luks_pbkdf_time_cost_usec(hr) / USEC_PER_MSEC, + .max_memory_kb = user_record_luks_pbkdf_memory_cost(hr) / 1024, + .parallel_threads = user_record_luks_pbkdf_parallel_threads(hr), + }, + }); + if (r < 0) + return log_error_errno(r, "Failed to format LUKS image: %m"); + + log_info("LUKS formatting completed."); + + STRV_FOREACH(pp, passwords) { + r = test_password(hashed_passwords, *pp); + if (r < 0) + return log_error_errno(r, "Failed to verify whether password is valid: %m"); + if (r == 0) /* Ignore passwords that are not available as hashed too, it's not supposed to be + * used for disk encryption (might be for smartcard?) */ + continue; + + r = crypt_keyslot_add_by_volume_key( + cd, + slot, + volume_key, + volume_key_size, + *pp, + strlen(*pp)); + if (r < 0) + return log_error_errno(r, "Failed to set up LUKS password for slot %i: %m", slot); + + log_info("Writing password to LUKS keyslot %i completed.", slot); + added = true; + } + + if (!added) + return log_error_errno(r, "No password configured on LUKS, refusing."); + + r = crypt_activate_by_volume_key( + cd, + dm_name, + volume_key, + volume_key_size, + discard ? CRYPT_ACTIVATE_ALLOW_DISCARDS : 0); + if (r < 0) + return log_error_errno(r, "Failed to activate LUKS superblock: %m"); + + log_info("LUKS activation by volume key succeeded."); + + r = user_record_clone(hr, USER_RECORD_EXTRACT_EMBEDDED, &reduced); + if (r < 0) + return log_error_errno(r, "Failed to prepare home record for LUKS: %m"); + + r = format_luks_token_text(cd, reduced, volume_key, &text); + if (r < 0) + return r; + + r = crypt_token_json_set(cd, CRYPT_ANY_TOKEN, text); + if (r < 0) + return log_error_errno(r, "Failed to set LUKS JSON token: %m"); + + log_info("Writing user record as LUKS token completed."); + + if (ret) + *ret = TAKE_PTR(cd); + + return 0; +} + +static int copy_skel(int root_fd, const char *skel) { + int r; + + assert(root_fd >= 0); + + r = copy_tree_at(AT_FDCWD, skel, root_fd, ".", UID_INVALID, GID_INVALID, COPY_MERGE|COPY_REPLACE); + if (r == -ENOENT) { + log_info("Skeleton directory %s missing, ignoring.", skel); + return 0; + } + if (r < 0) + return log_error_errno(r, "Failed to copy in %s: %m", skel); + + log_info("Copying in %s completed.", skel); + return 0; +} + +static int change_access_mode(int root_fd, mode_t m) { + assert(root_fd >= 0); + + if (fchmod(root_fd, m) < 0) + return log_error_errno(errno, "Failed to change access mode of top-level directory: %m"); + + log_info("Changed top-level directory access mode to 0%o.", m); + return 0; +} + +static int home_populate(UserRecord *h, int dir_fd) { + int r; + + assert(h); + assert(dir_fd >= 0); + + r = copy_skel(dir_fd, user_record_skeleton_directory(h)); + if (r < 0) + return r; + + r = store_embedded_identity(h, dir_fd, h->uid, NULL); + if (r < 0) + return r; + + r = chown_recursive_directory(dir_fd, h->uid); + if (r < 0) + return r; + + r = change_access_mode(dir_fd, user_record_access_mode(h)); + if (r < 0) + return r; + + return 0; +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_context*, fdisk_unref_context); +DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_partition*, fdisk_unref_partition); +DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_parttype*, fdisk_unref_parttype); +DEFINE_TRIVIAL_CLEANUP_FUNC(struct fdisk_table*, fdisk_unref_table); + +static int make_partition_table( + int fd, + const char *label, + sd_id128_t uuid, + uint64_t *ret_offset, + uint64_t *ret_size, + sd_id128_t *ret_disk_uuid) { + + _cleanup_(fdisk_unref_partitionp) struct fdisk_partition *p = NULL, *q = NULL; + _cleanup_free_ char *path = NULL, *joined = NULL, *disk_uuid_as_string = NULL; + _cleanup_(fdisk_unref_parttypep) struct fdisk_parttype *t = NULL; + _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL; + uint64_t offset, size; + sd_id128_t disk_uuid; + char uuids[37]; + int r; + + assert(fd >= 0); + assert(label); + assert(ret_offset); + assert(ret_size); + + t = fdisk_new_parttype(); + if (!t) + return log_oom(); + + r = fdisk_parttype_set_typestr(t, "773f91ef-66d4-49b5-bd83-d683bf40ad16"); + if (r < 0) + return log_error_errno(r, "Failed to initialize partition type: %m"); + + c = fdisk_new_context(); + if (!c) + return log_oom(); + + if (asprintf(&path, "/proc/self/fd/%i", fd) < 0) + return log_oom(); + + r = fdisk_assign_device(c, path, 0); + if (r < 0) + return log_error_errno(r, "Failed to open device: %m"); + + r = fdisk_create_disklabel(c, "gpt"); + if (r < 0) + return log_error_errno(r, "Failed to create gpt disk label: %m"); + + p = fdisk_new_partition(); + if (!p) + return log_oom(); + + r = fdisk_partition_set_type(p, t); + if (r < 0) + return log_error_errno(r, "Failed to set partition type: %m"); + + r = fdisk_partition_start_follow_default(p, 1); + if (r < 0) + return log_error_errno(r, "Failed to place partition at beginning of space: %m"); + + r = fdisk_partition_partno_follow_default(p, 1); + if (r < 0) + return log_error_errno(r, "Failed to place partition at first free partition index: %m"); + + r = fdisk_partition_end_follow_default(p, 1); + if (r < 0) + return log_error_errno(r, "Failed to make partition cover all free space: %m"); + + r = fdisk_partition_set_name(p, label); + if (r < 0) + return log_error_errno(r, "Failed to set partition name: %m"); + + r = fdisk_partition_set_uuid(p, id128_to_uuid_string(uuid, uuids)); + if (r < 0) + return log_error_errno(r, "Failed to set partition UUID: %m"); + + r = fdisk_add_partition(c, p, NULL); + if (r < 0) + return log_error_errno(r, "Failed to add partition: %m"); + + r = fdisk_write_disklabel(c); + if (r < 0) + return log_error_errno(r, "Failed to write disk label: %m"); + + r = fdisk_get_disklabel_id(c, &disk_uuid_as_string); + if (r < 0) + return log_error_errno(r, "Failed to determine disk label UUID: %m"); + + r = sd_id128_from_string(disk_uuid_as_string, &disk_uuid); + if (r < 0) + return log_error_errno(r, "Failed to parse disk label UUID: %m"); + + r = fdisk_get_partition(c, 0, &q); + if (r < 0) + return log_error_errno(r, "Failed to read created partition metadata: %m"); + + assert(fdisk_partition_has_start(q)); + offset = fdisk_partition_get_start(q); + if (offset > UINT64_MAX / 512U) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Partition offset too large."); + + assert(fdisk_partition_has_size(q)); + size = fdisk_partition_get_size(q); + if (size > UINT64_MAX / 512U) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Partition size too large."); + + *ret_offset = offset * 512U; + *ret_size = size * 512U; + *ret_disk_uuid = disk_uuid; + + return 0; +} + +static bool supported_fs_size(const char *fstype, uint64_t host_size) { + uint64_t m; + + m = minimal_size_by_fs_name(fstype); + if (m == UINT64_MAX) + return false; + + return host_size >= m; +} + +static int wait_for_devlink(const char *path) { + _cleanup_close_ int inotify_fd = -1; + usec_t until; + int r; + + /* let's wait for a device link to show up in /dev, with a time-out. This is good to do since we + * return a /dev/disk/by-uuid/… link to our callers and they likely want to access it right-away, + * hence let's wait until udev has caught up with our changes, and wait for the symlink to be + * created. */ + + until = usec_add(now(CLOCK_MONOTONIC), 45 * USEC_PER_SEC); + + for (;;) { + _cleanup_free_ char *dn = NULL; + usec_t w; + + if (laccess(path, F_OK) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to determine whether %s exists: %m", path); + } else + return 0; /* Found it */ + + if (inotify_fd < 0) { + /* We need to wait for the device symlink to show up, let's create an inotify watch for it */ + inotify_fd = inotify_init1(IN_NONBLOCK|IN_CLOEXEC); + if (inotify_fd < 0) + return log_error_errno(errno, "Failed to allocate inotify fd: %m"); + } + + dn = dirname_malloc(path); + for (;;) { + if (!dn) + return log_oom(); + + log_info("Watching %s", dn); + + if (inotify_add_watch(inotify_fd, dn, IN_CREATE|IN_MOVED_TO|IN_ONLYDIR|IN_DELETE_SELF|IN_MOVE_SELF) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to add watch on %s: %m", dn); + } else + break; + + if (empty_or_root(dn)) + break; + + dn = dirname_malloc(dn); + } + + w = now(CLOCK_MONOTONIC); + if (w >= until) + return log_error_errno(SYNTHETIC_ERRNO(ETIMEDOUT), "Device link %s still hasn't shown up, giving up.", path); + + r = fd_wait_for_event(inotify_fd, POLLIN, usec_sub_unsigned(until, w)); + if (r < 0) + return log_error_errno(r, "Failed to watch inotify: %m"); + + (void) flush_fd(inotify_fd); + } +} + +static int calculate_disk_size(UserRecord *h, const char *parent_dir, uint64_t *ret) { + char buf[FORMAT_BYTES_MAX]; + struct statfs sfs; + uint64_t m; + + assert(h); + assert(parent_dir); + assert(ret); + + if (h->disk_size != UINT64_MAX) { + *ret = DISK_SIZE_ROUND_DOWN(h->disk_size); + return 0; + } + + if (statfs(parent_dir, &sfs) < 0) + return log_error_errno(errno, "statfs() on %s failed: %m", parent_dir); + + m = sfs.f_bsize * sfs.f_bavail; + + if (h->disk_size_relative == UINT64_MAX) { + + if (m > UINT64_MAX / USER_DISK_SIZE_DEFAULT_PERCENT) + return log_error_errno(SYNTHETIC_ERRNO(EOVERFLOW), "Disk size too large."); + + *ret = DISK_SIZE_ROUND_DOWN(m * USER_DISK_SIZE_DEFAULT_PERCENT / 100); + + log_info("Sizing home to %u%% of available disk space, which is %s.", + USER_DISK_SIZE_DEFAULT_PERCENT, + format_bytes(buf, sizeof(buf), *ret)); + } else { + *ret = DISK_SIZE_ROUND_DOWN((uint64_t) ((double) m * (double) h->disk_size_relative / (double) UINT32_MAX)); + + log_info("Sizing home to %" PRIu64 ".%01" PRIu64 "%% of available disk space, which is %s.", + (h->disk_size_relative * 100) / UINT32_MAX, + ((h->disk_size_relative * 1000) / UINT32_MAX) % 10, + format_bytes(buf, sizeof(buf), *ret)); + } + + if (*ret < USER_DISK_SIZE_MIN) + *ret = USER_DISK_SIZE_MIN; + + return 0; +} + +static int home_create_luks(UserRecord *h, UserRecord **ret_home) { + _cleanup_free_ char *dm_name = NULL, *dm_node = NULL, *subdir = NULL, *disk_uuid_path = NULL, *temporary_image_path = NULL; + uint64_t host_size, encrypted_size, partition_offset, partition_size; + bool image_created = false, dm_activated = false, mounted = false; + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + sd_id128_t partition_uuid, fs_uuid, luks_uuid, disk_uuid; + _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL; + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_close_ int image_fd = -1, root_fd = -1; + const char *fstype, *ip; + struct statfs sfs; + int r; + + assert(h); + assert(h->storage < 0 || h->storage == USER_LUKS); + assert(ret_home); + + assert_se(ip = user_record_image_path(h)); + + fstype = user_record_file_system_type(h); + if (!supported_fstype(fstype)) + return log_error_errno(SYNTHETIC_ERRNO(EPROTONOSUPPORT), "Unsupported file system type: %s", h->file_system_type); + + if (sd_id128_is_null(h->partition_uuid)) { + r = sd_id128_randomize(&partition_uuid); + if (r < 0) + return log_error_errno(r, "Failed to acquire partition UUID: %m"); + } else + partition_uuid = h->partition_uuid; + + if (sd_id128_is_null(h->luks_uuid)) { + r = sd_id128_randomize(&luks_uuid); + if (r < 0) + return log_error_errno(r, "Failed to acquire LUKS UUID: %m"); + } else + luks_uuid = h->luks_uuid; + + if (sd_id128_is_null(h->file_system_uuid)) { + r = sd_id128_randomize(&fs_uuid); + if (r < 0) + return log_error_errno(r, "Failed to acquire file system UUID: %m"); + } else + fs_uuid = h->file_system_uuid; + + r = make_dm_names(h->user_name, &dm_name, &dm_node); + if (r < 0) + return r; + + r = access(dm_node, F_OK); + if (r < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to determine whether %s exists: %m", dm_node); + } else + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Device mapper device %s already exists, refusing.", dm_node); + + if (path_startswith(ip, "/dev/")) { + _cleanup_free_ char *sysfs = NULL; + uint64_t block_device_size; + struct stat st; + + /* Let's place the home directory on a real device, i.e. an USB stick or such */ + + image_fd = open(ip, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (image_fd < 0) + return log_error_errno(errno, "Failed to open device %s: %m", ip); + + if (fstat(image_fd, &st) < 0) + return log_error_errno(errno, "Failed to stat device %s: %m", ip); + if (!S_ISBLK(st.st_mode)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Device is not a block device, refusing."); + + if (asprintf(&sysfs, "/sys/dev/block/%u:%u/partition", major(st.st_rdev), minor(st.st_rdev)) < 0) + return log_oom(); + if (access(sysfs, F_OK) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to check whether %s exists: %m", sysfs); + } else + return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Operating on partitions is currently not supported, sorry. Please specify a top-level block device."); + + if (flock(image_fd, LOCK_EX) < 0) /* make sure udev doesn't read from it while we operate on the device */ + return log_error_errno(errno, "Failed to lock block device %s: %m", ip); + + if (ioctl(image_fd, BLKGETSIZE64, &block_device_size) < 0) + return log_error_errno(errno, "Failed to read block device size: %m"); + + if (h->disk_size == UINT64_MAX) { + + /* If a relative disk size is requested, apply it relative to the block device size */ + if (h->disk_size_relative < UINT32_MAX) + host_size = CLAMP(DISK_SIZE_ROUND_DOWN(block_device_size * h->disk_size_relative / UINT32_MAX), + USER_DISK_SIZE_MIN, USER_DISK_SIZE_MAX); + else + host_size = block_device_size; /* Otherwise, take the full device */ + + } else if (host_size > block_device_size) + return log_error_errno(SYNTHETIC_ERRNO(EMSGSIZE), "Selected disk size larger than backing block device, refusing."); + else + host_size = DISK_SIZE_ROUND_DOWN(h->disk_size); + + if (!supported_fs_size(fstype, host_size)) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Selected file system size too small for %s.", h->file_system_type); + + /* After creation we should reference this partition by its UUID instead of the block + * device. That's preferable since the user might have specified a device node such as + * /dev/sdb to us, which might look very different when replugged. */ + if (asprintf(&disk_uuid_path, "/dev/disk/by-uuid/" SD_ID128_UUID_FORMAT_STR, SD_ID128_FORMAT_VAL(luks_uuid)) < 0) + return log_oom(); + + if (user_record_luks_discard(h)) { + if (ioctl(image_fd, BLKDISCARD, (uint64_t[]) { 0, block_device_size }) < 0) + log_full_errno(errno == EOPNOTSUPP ? LOG_DEBUG : LOG_WARNING, errno, + "Failed to issue full-device BLKDISCARD on device, ignoring: %m"); + else + log_info("Full device discard completed."); + } + } else { + _cleanup_free_ char *parent = NULL; + + parent = dirname_malloc(ip); + if (!parent) + return log_oom(); + + r = mkdir_p(parent, 0755); + if (r < 0) + return log_error_errno(r, "Failed to create parent directory %s: %m", parent); + + r = calculate_disk_size(h, parent, &host_size); + if (r < 0) + return r; + + if (!supported_fs_size(fstype, host_size)) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Selected file system size too small for %s.", h->file_system_type); + + r = tempfn_random(ip, "homework", &temporary_image_path); + if (r < 0) + return log_error_errno(r, "Failed to derive temporary file name for %s: %m", ip); + + image_fd = open(temporary_image_path, O_RDWR|O_CREAT|O_EXCL|O_CLOEXEC|O_NOCTTY|O_NOFOLLOW, 0600); + if (image_fd < 0) + return log_error_errno(errno, "Failed to create home image %s: %m", temporary_image_path); + + image_created = true; + + r = chattr_fd(image_fd, FS_NOCOW_FL, FS_NOCOW_FL, NULL); + if (r < 0) + log_warning_errno(r, "Failed to set file attributes on %s, ignoring: %m", temporary_image_path); + + if (user_record_luks_discard(h)) + r = ftruncate(image_fd, host_size); + else + r = fallocate(image_fd, 0, 0, host_size); + if (r < 0) { + if (ERRNO_IS_DISK_SPACE(errno)) { + log_debug_errno(errno, "Not enough disk space to allocate home."); + r = -ENOSPC; /* make recognizable */ + goto fail; + } + + r = log_error_errno(errno, "Failed to truncate home image %s: %m", temporary_image_path); + goto fail; + } + + log_info("Allocating image file completed."); + } + + r = make_partition_table( + image_fd, + user_record_user_name_and_realm(h), + partition_uuid, + &partition_offset, + &partition_size, + &disk_uuid); + if (r < 0) + goto fail; + + log_info("Writing of partition table completed."); + + r = loop_device_make_full(image_fd, O_RDWR, partition_offset, partition_size, 0, &loop); + if (r < 0) { + if (r == -ENOENT) { /* this means /dev/loop-control doesn't exist, i.e. we are in a container + * or similar and loopback bock devices are not available, return a + * recognizable error in this case. */ + log_error_errno(r, "Loopback block device support is not available on this system."); + r = -ENOLINK; + goto fail; + } + + log_error_errno(r, "Failed to set up loopback device for %s: %m", temporary_image_path); + goto fail; + } + + r = loop_device_flock(loop, LOCK_EX); /* make sure udev won't read before we are done */ + if (r < 0) { + log_error_errno(r, "Failed to take lock on loop device: %m"); + goto fail; + } + + log_info("Setting up loopback device %s completed.", loop->node ?: ip); + + r = luks_format(loop->node, dm_name, luks_uuid, user_record_user_name_and_realm(h), h->password, h->hashed_password, user_record_luks_discard(h), h, &cd); + if (r < 0) + goto fail; + + dm_activated = true; + + r = block_get_size_by_path(dm_node, &encrypted_size); + if (r < 0) { + log_error_errno(r, "Failed to get encrypted block device size: %m"); + goto fail; + } + + log_info("Setting up LUKS device %s completed.", dm_node); + + r = run_mkfs(dm_node, fstype, user_record_user_name_and_realm(h), fs_uuid, user_record_luks_discard(h)); + if (r < 0) + goto fail; + + log_info("Formatting file system completed."); + + r = unshare_and_mount(dm_node, fstype, user_record_luks_discard(h)); + if (r < 0) + goto fail; + + mounted = true; + + subdir = path_join("/run/systemd/user-home-mount/", user_record_user_name_and_realm(h)); + if (!subdir) { + r = log_oom(); + goto fail; + } + + if (mkdir(subdir, 0700) < 0) { + r = log_error_errno(errno, "Failed to create user directory in mounted image file: %m"); + goto fail; + } + + root_fd = open(subdir, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) { + r = log_error_errno(errno, "Failed to open user directory in mounted image file: %m"); + goto fail; + } + + r = home_populate(h, root_fd); + if (r < 0) + goto fail; + + r = sync_and_statfs(root_fd, &sfs); + if (r < 0) + goto fail; + + r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG, &new_home); + if (r < 0) { + log_error_errno(r, "Failed to clone record: %m"); + goto fail; + } + + r = user_record_add_binding( + new_home, + USER_LUKS, + disk_uuid_path ?: ip, + partition_uuid, + luks_uuid, + fs_uuid, + crypt_get_cipher(cd), + crypt_get_cipher_mode(cd), + luks_volume_key_size_convert(cd), + fstype, + NULL, 0, + NULL, + h->uid, + (gid_t) h->uid); + if (r < 0) { + log_error_errno(r, "Failed to add binding to record: %m"); + goto fail; + } + + root_fd = safe_close(root_fd); + + r = umount_verbose("/run/systemd/user-home-mount"); + if (r < 0) + goto fail; + + mounted = false; + + r = crypt_deactivate(cd, dm_name); + if (r < 0) { + log_error_errno(r, "Failed to deactivate LUKS device: %m"); + goto fail; + } + + dm_activated = false; + + loop = loop_device_unref(loop); + + if (disk_uuid_path) + (void) ioctl(image_fd, BLKRRPART, 0); + + /* Let's close the image fd now. If we are operating on a real block device this will release the BSD + * lock that ensures udev doesn't interfere with what we are doing */ + image_fd = safe_close(image_fd); + + if (temporary_image_path) { + if (rename(temporary_image_path, ip) < 0) { + log_error_errno(errno, "Failed to rename image file: %m"); + goto fail; + } + + log_info("Moved image file into place."); + } + + if (disk_uuid_path) + (void) wait_for_devlink(disk_uuid_path); + + log_info("Everything completed."); + + print_size_summary(host_size, encrypted_size, &sfs); + + *ret_home = TAKE_PTR(new_home); + return 0; + +fail: + /* Let's close all files before we unmount the file system, to avoid EBUSY */ + root_fd = safe_close(root_fd); + + if (mounted) + (void) umount_verbose("/run/systemd/user-home-mount"); + + if (dm_activated) + (void) crypt_deactivate(cd, dm_name); + + loop = loop_device_unref(loop); + + if (image_created) + (void) unlink(temporary_image_path); + + return r; +} + +static int home_update_quota_btrfs(UserRecord *h, const char *path) { + int r; + + assert(h); + assert(path); + + if (h->disk_size == UINT64_MAX) + return 0; + + /* If the user wants quota, enable it */ + r = btrfs_quota_enable(path, true); + if (r == -ENOTTY) + return log_error_errno(r, "No btrfs quota support on subvolume %s.", path); + if (r < 0) + return log_error_errno(r, "Failed to enable btrfs quota support on %s.", path); + + r = btrfs_qgroup_set_limit(path, 0, h->disk_size); + if (r < 0) + return log_error_errno(r, "Faled to set disk quota on subvolume %s: %m", path); + + log_info("Set btrfs quota."); + + return 0; +} + +static int home_update_quota_classic(UserRecord *h, const char *path) { + _cleanup_free_ char *devnode = NULL; + struct dqblk req; + dev_t devno; + int r; + + assert(h); + assert(uid_is_valid(h->uid)); + assert(path); + + if (h->disk_size == UINT64_MAX) + return 0; + + r = get_block_device(path, &devno); + if (r < 0) + return log_error_errno(r, "Failed to determine block device of %s: %m", path); + if (devno == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENODEV), "File system %s not backed by a block device.", path); + + r = device_path_make_major_minor(S_IFBLK, devno, &devnode); + if (r < 0) + return log_error_errno(r, "Failed to derive block device path for file system %s: %m", path); + + if (quotactl(QCMD(Q_GETQUOTA, USRQUOTA), devnode, h->uid, (caddr_t) &req) < 0) { + if (ERRNO_IS_NOT_SUPPORTED(errno)) + return log_error_errno(errno, "No UID quota support on %s.", path); + + if (errno != ESRCH) + return log_error_errno(errno, "Failed to query disk quota for UID " UID_FMT ": %m", h->uid); + + zero(req); + } else { + /* Shortcut things if everything is set up properly already */ + if (FLAGS_SET(req.dqb_valid, QIF_BLIMITS) && h->disk_size / QIF_DQBLKSIZE == req.dqb_bhardlimit) { + log_info("Configured quota already matches the intended setting, not updating quota."); + return 0; + } + } + + req.dqb_valid = QIF_BLIMITS; + req.dqb_bsoftlimit = req.dqb_bhardlimit = h->disk_size / QIF_DQBLKSIZE; + + if (quotactl(QCMD(Q_SETQUOTA, USRQUOTA), devnode, h->uid, (caddr_t) &req) < 0) { + if (errno == ESRCH) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "UID quota not available on %s.", path); + + return log_error_errno(errno, "Failed to set disk quota for UID " UID_FMT ": %m", h->uid); + } + + log_info("Updated per-UID quota."); + + return 0; +} + +static int home_update_quota_auto(UserRecord *h, const char *path) { + struct statfs sfs; + int r; + + assert(h); + + if (h->disk_size == UINT64_MAX) + return 0; + + if (!path) { + path = user_record_image_path(h); + if (!path) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Home record lacks image path."); + } + + if (statfs(path, &sfs) < 0) + return log_error_errno(errno, "Failed to statfs() file system: %m"); + + if (is_fs_type(&sfs, XFS_SB_MAGIC) || + is_fs_type(&sfs, EXT4_SUPER_MAGIC)) + return home_update_quota_classic(h, path); + + if (is_fs_type(&sfs, BTRFS_SUPER_MAGIC)) { + + r = btrfs_is_subvol(path); + if (r < 0) + return log_error_errno(errno, "Failed to test if %s is a subvolume: %m", path); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Directory %s is not a subvolume, cannot apply quota.", path); + + return home_update_quota_btrfs(h, path); + } + + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Type of directory %s not known, cannot apply quota.", path); +} + +static int home_create_directory_or_subvolume(UserRecord *h, UserRecord **ret_home) { + _cleanup_(rm_rf_subvolume_and_freep) char *temporary = NULL; + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + _cleanup_close_ int root_fd = -1; + _cleanup_free_ char *d = NULL; + const char *ip; + int r; + + assert(h); + assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME)); + assert(ret_home); + + assert_se(ip = user_record_image_path(h)); + + r = tempfn_random(ip, "homework", &d); + if (r < 0) + return log_error_errno(r, "Failed to allocate temporary directory: %m"); + + (void) mkdir_parents(d, 0755); + + switch (user_record_storage(h)) { + + case USER_SUBVOLUME: + RUN_WITH_UMASK(0077) + r = btrfs_subvol_make(d); + + if (r >= 0) { + log_info("Subvolume created."); + + if (h->disk_size != UINT64_MAX) { + + /* Enable quota for the subvolume we just created. Note we don't check for + * errors here and only log about debug level about this. */ + r = btrfs_quota_enable(d, true); + if (r < 0) + log_debug_errno(r, "Failed to enable quota on %s, ignoring: %m", d); + + r = btrfs_subvol_auto_qgroup(d, 0, false); + if (r < 0) + log_debug_errno(r, "Failed to set up automatic quota group on %s, ignoring: %m", d); + + /* Actually configure the quota. We also ignore errors here, but we do log + * about them loudly, to keep things discoverable even though we don't + * consider lacking quota support in kernel fatal. */ + (void) home_update_quota_btrfs(h, d); + } + + break; + } + if (r != -ENOTTY) + return log_error_errno(r, "Failed to create temporary home directory subvolume %s: %m", d); + + log_info("Creating subvolume %s is not supported, as file system does not support subvolumes. Falling back to regular directory.", d); + _fallthrough_; + + case USER_DIRECTORY: + + if (mkdir(d, 0700) < 0) + return log_error_errno(errno, "Failed to create temporary home directory %s: %m", d); + + (void) home_update_quota_classic(h, d); + break; + + default: + assert_not_reached("unexpected storage"); + } + + temporary = TAKE_PTR(d); /* Needs to be destroyed now */ + + root_fd = open(temporary, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) + return log_error_errno(errno, "Failed to open temporary home directory: %m"); + + r = home_populate(h, root_fd); + if (r < 0) + return r; + + r = sync_and_statfs(root_fd, NULL); + if (r < 0) + return r; + + r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET, &new_home); + if (r < 0) + return log_error_errno(r, "Failed to clone record: %m"); + + r = user_record_add_binding( + new_home, + user_record_storage(h), + ip, + SD_ID128_NULL, + SD_ID128_NULL, + SD_ID128_NULL, + NULL, + NULL, + UINT64_MAX, + NULL, + NULL, 0, + NULL, + h->uid, + (gid_t) h->uid); + if (r < 0) + return log_error_errno(r, "Failed to add binding to record: %m"); + + if (rename(temporary, ip) < 0) + return log_error_errno(errno, "Failed to rename %s to %s: %m", temporary, ip); + + temporary = mfree(temporary); + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 0; +} + +static int home_create_fscrypt(UserRecord *h, UserRecord **ret_home) { + _cleanup_free_ char *d = NULL, *hex = NULL, *salt_xattr = NULL; + _cleanup_(rm_rf_physical_and_freep) char *temporary = NULL; + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + _cleanup_close_ int root_fd = -1; + struct fscrypt_policy policy; + uint8_t salt_buffer[64]; + const void *salt = NULL; + size_t salt_size = 0; + const char *ip; + int r; + + assert(h); + assert(user_record_storage(h) == USER_FSCRYPT); + assert(ret_home); + + if (strv_length(h->password) > 1) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Record has more than one password which is not supported for fscrypt, refusing."); + + assert_se(ip = user_record_image_path(h)); + + r = tempfn_random(ip, "homework", &d); + if (r < 0) + return log_error_errno(r, "Failed to allocate temporary directory: %m"); + + (void) mkdir_parents(d, 0755); + + if (mkdir(d, 0700) < 0) + return log_error_errno(errno, "Failed to create temporary home directory %s: %m", d); + + temporary = TAKE_PTR(d); /* Needs to be destroyed now */ + + root_fd = open(temporary, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) + return log_error_errno(errno, "Failed to open temporary home directory: %m"); + + if (ioctl(root_fd, FS_IOC_GET_ENCRYPTION_POLICY, &policy) < 0) { + if (ERRNO_IS_NOT_SUPPORTED(errno)) { + log_error_errno(errno, "File system does not support fscrypt: %m"); + return -ENOLINK; /* make recognizable */ + } + if (errno != ENODATA) + return log_error_errno(errno, "Failed to get fscrypt policy of directory: %m"); + } else + return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Parent of %s already encrypted, refusing.", d); + + policy = (struct fscrypt_policy) { + .version = 0, + .contents_encryption_mode = FS_ENCRYPTION_MODE_AES_256_XTS, + .filenames_encryption_mode = FS_ENCRYPTION_MODE_AES_256_CTS, + .flags = FS_POLICY_FLAGS_PAD_32, + }; + + if (h->fscrypt_salt_size > 0) { + salt = h->fscrypt_salt; + salt_size = h->fscrypt_salt_size; + } else { + ssize_t n; + + r = genuine_random_bytes(salt_buffer, sizeof(salt_buffer), RANDOM_BLOCK); + if (r < 0) + return log_error_errno(r, "Failed to generate salt value: %m"); + + n = base64mem(salt_buffer, sizeof(salt_buffer), &salt_xattr); + if (n < 0) + return log_error_errno(n, "Failed to base64 encode salt value: %m"); + + salt = salt_buffer; + salt_size = sizeof(salt_buffer); + } + + r = home_upload_fscrypt_key(h, h->password[0], salt, salt_size, NULL, policy.master_key_descriptor); + if (r < 0) + return r; + + if (ioctl(root_fd, FS_IOC_SET_ENCRYPTION_POLICY, &policy) < 0) + return log_error_errno(errno, "Failed to set fscrypt policy on directory: %m"); + + log_info("Encryption policy set."); + + (void) home_update_quota_classic(h, temporary); + + r = home_populate(h, root_fd); + if (r < 0) + return r; + + if (fsetxattr(root_fd, "trusted.fscrypt_salt", salt_xattr, strlen(salt_xattr), 0) < 0) + return log_error_errno(errno, "Failed to set salt value extended attribute on %s: %m", ip); + + r = sync_and_statfs(root_fd, NULL); + if (r < 0) + return r; + + r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET, &new_home); + if (r < 0) + return log_error_errno(r, "Failed to clone record: %m"); + + r = user_record_add_binding( + new_home, + USER_FSCRYPT, + ip, + SD_ID128_NULL, + SD_ID128_NULL, + SD_ID128_NULL, + NULL, + NULL, + UINT64_MAX, + NULL, + salt, salt_size, + NULL, + h->uid, + (gid_t) h->uid); + if (r < 0) + return log_error_errno(r, "Failed to add binding to record: %m"); + + if (rename(temporary, ip) < 0) + return log_error_errno(errno, "Failed to rename %s to %s: %m", temporary, ip); + + temporary = mfree(temporary); + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 0; +} + +static int home_create_cifs(UserRecord *h, UserRecord **ret_home) { + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + _cleanup_(closedirp) DIR *d = NULL; + int r, copy; + + assert(h); + assert(user_record_storage(h) == USER_CIFS); + assert(ret_home); + + if (!h->cifs_service) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks CIFS service, refusing."); + + if (access("/sbin/mount.cifs", F_OK) < 0) { + if (errno == ENOENT) + return log_error_errno(SYNTHETIC_ERRNO(ENOLINK), "/sbin/mount.cifs is missing."); + + return log_error_errno(errno, "Unable to detect whether /sbin/mount.cifs exists: %m"); + } + + r = home_prepare_cifs(h, false, &setup); + if (r < 0) + return r; + + copy = fcntl(setup.root_fd, F_DUPFD_CLOEXEC, 3); + if (copy < 0) + return -errno; + + d = fdopendir(copy); + if (!d) { + safe_close(copy); + return -errno; + } + + errno = 0; + if (readdir_no_dot(d)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTEMPTY), "Selected CIFS directory not empty, refusing."); + if (errno != 0) + return log_error_errno(errno, "Failed to detect if CIFS directory is empty: %m"); + + r = home_populate(h, setup.root_fd); + if (r < 0) + return r; + + r = sync_and_statfs(setup.root_fd, NULL); + if (r < 0) + return r; + + r = user_record_clone(h, USER_RECORD_LOAD_MASK_SECRET, &new_home); + if (r < 0) + return log_error_errno(r, "Failed to clone record: %m"); + + r = user_record_add_binding( + new_home, + USER_CIFS, + NULL, + SD_ID128_NULL, + SD_ID128_NULL, + SD_ID128_NULL, + NULL, + NULL, + UINT64_MAX, + NULL, + NULL, 0, + NULL, + h->uid, + (gid_t) h->uid); + if (r < 0) + return log_error_errno(r, "Failed to add binding to record: %m"); + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 0; +} + +static int home_create(UserRecord *h, UserRecord **ret_home) { + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; + int r; + + assert(h); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks name, refusing."); + if (!uid_is_valid(h->uid)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks UID, refusing."); + + r = user_record_validate_hashed_password(h); + if (r < 0) + return r; + + r = user_record_test_home_directory_and_warn(h); + if (r < 0) + return r; + if (r != USER_TEST_ABSENT) + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Home directory %s already exists, refusing.", user_record_home_directory(h)); + + /* When the user didn't specify the storage type to use, fix it to be LUKS -- unless we run in a + * container where loopback devices and LUKS/DM are not available. Note that we typically default to + * the assumption of "classic" storage for most operations. However, if we create a new home, then + * let's user LUKS if nothing is specified. */ + if (h->storage < 0) { + UserStorage new_storage; + + r = detect_container(); + if (r < 0) + return log_error_errno(r, "Failed to determine whether we are in a container: %m"); + if (r > 0) { + new_storage = USER_DIRECTORY; + + r = path_is_fs_type("/home", BTRFS_SUPER_MAGIC); + if (r < 0) + log_debug_errno(r, "Failed to determine file system of /home, ignoring: %m"); + + new_storage = r > 0 ? USER_SUBVOLUME : USER_DIRECTORY; + } else + new_storage = USER_LUKS; + + r = user_record_add_binding( + h, + new_storage, + NULL, + SD_ID128_NULL, + SD_ID128_NULL, + SD_ID128_NULL, + NULL, + NULL, + UINT64_MAX, + NULL, + NULL, 0, + NULL, + UID_INVALID, + GID_INVALID); + if (r < 0) + return log_error_errno(r, "Failed to change storage type to LUKS: %m"); + + if (!h->image_path_auto) { + h->image_path_auto = strjoin("/home/", user_record_user_name_and_realm(h), new_storage == USER_LUKS ? ".home" : ".homedir"); + if (!h->image_path_auto) + return log_oom(); + } + } + + r = user_record_test_image_path_and_warn(h); + if (r < 0) + return r; + if (!IN_SET(r, USER_TEST_ABSENT, USER_TEST_UNDEFINED, USER_TEST_MAYBE)) + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Image path %s already exists, refusing.", user_record_image_path(h)); + + switch (user_record_storage(h)) { + + case USER_LUKS: + r = home_create_luks(h, &new_home); + break; + + case USER_DIRECTORY: + case USER_SUBVOLUME: + r = home_create_directory_or_subvolume(h, &new_home); + break; + + case USER_FSCRYPT: + r = home_create_fscrypt(h, &new_home); + break; + + case USER_CIFS: + r = home_create_cifs(h, &new_home); + break; + + default: + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), + "Creating home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + } + if (r < 0) + return r; + + if (user_record_equal(h, new_home)) { + *ret_home = NULL; + return 0; + } + + *ret_home = TAKE_PTR(new_home); + return 1; +} + +static int home_remove(UserRecord *h) { + bool deleted = false; + const char *ip, *hd; + int r; + + assert(h); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks user name, refusing."); + if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Removing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + hd = user_record_home_directory(h); + + r = user_record_test_home_directory_and_warn(h); + if (r < 0) + return r; + if (r == USER_TEST_MOUNTED) + return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Directory %s is still mounted, refusing.", hd); + + assert(hd); + + r = user_record_test_image_path_and_warn(h); + if (r < 0) + return r; + + ip = user_record_image_path(h); + + switch (user_record_storage(h)) { + + case USER_LUKS: { + struct stat st; + + assert(ip); + + if (stat(ip, &st) < 0) { + if (errno != -ENOENT) + return log_error_errno(errno, "Failed to stat %s: %m", ip); + + } else { + if (S_ISREG(st.st_mode)) { + if (unlink(ip) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to remove %s: %m", ip); + } else + deleted = true; + + } else if (S_ISBLK(st.st_mode)) + log_info("Not removing file system on block device %s.", ip); + else + return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Image file %s is neither block device, nor regular, refusing removal.", ip); + } + + break; + } + + case USER_SUBVOLUME: + case USER_DIRECTORY: + case USER_FSCRYPT: + assert(ip); + + r = rm_rf(ip, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_SUBVOLUME); + if (r < 0) { + if (r != -ENOENT) + return log_warning_errno(r, "Failed to remove %s: %m", ip); + } else + deleted = true; + + /* If the image path and the home directory are the same invalidate the home directory, so + * that we don't remove it anymore */ + if (path_equal(ip, hd)) + hd = NULL; + + break; + + case USER_CIFS: + /* Nothing else to do here: we won't remove remote stuff. */ + log_info("Not removing home directory on remote server."); + break; + + default: + assert_not_reached("unknown storage type"); + } + + if (hd) { + if (rmdir(hd) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to remove %s, ignoring: %m", hd); + } else + deleted = true; + } + + if (deleted) + log_info("Everything completed."); + else { + log_notice("Nothing to remove."); + return -EALREADY; + } + + return 0; +} + +static int home_validate_update(UserRecord *h, HomeSetup *setup) { + bool has_mount = false; + int r; + + assert(h); + assert(setup); + assert(!setup->dm_name); + assert(!setup->dm_node); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks user name, refusing."); + if (!uid_is_valid(h->uid)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record lacks UID, refusing."); + if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Processing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + r = user_record_test_home_directory_and_warn(h); + if (r < 0) + return r; + + has_mount = r == USER_TEST_MOUNTED; + + r = user_record_test_image_path_and_warn(h); + if (r < 0) + return r; + if (r == USER_TEST_ABSENT) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Image path %s does not exist", user_record_image_path(h)); + + switch (user_record_storage(h)) { + + case USER_DIRECTORY: + case USER_SUBVOLUME: + case USER_FSCRYPT: + case USER_CIFS: + break; + + case USER_LUKS: { + _cleanup_free_ char *dm_name = NULL, *dm_node = NULL; + bool has_dm = false; + + r = make_dm_names(h->user_name, &dm_name, &dm_node); + if (r < 0) + return r; + + r = access(dm_node, F_OK); + if (r < 0 && errno != ENOENT) + return log_error_errno(errno, "Failed to determine whether %s exists: %m", dm_node); + + has_dm = r >= 0; + + if (has_dm != has_mount) + return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Home mount incompletely set up."); + + setup->dm_name = TAKE_PTR(dm_name); + setup->dm_node = TAKE_PTR(dm_node); + break; + } + + default: + assert_not_reached("unexpected storage type"); + } + + return has_mount; /* return true if the home record is already active */ +} + +static int home_update(UserRecord *h, UserRecord **ret) { + _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *header_home = NULL, *embedded_home = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *embedded_json = NULL; + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + bool already_activated = false; + int r; + + assert(h); + assert(ret); + + r = user_record_validate_hashed_password(h); + if (r < 0) + return r; + + r = home_validate_update(h, &setup); + if (r < 0) + return r; + + already_activated = r > 0; + + r = home_prepare(h, already_activated, &setup, &header_home); + if (r < 0) + return r; + + r = load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER, &embedded_home, &new_home); + if (r < 0) + return r; + + r = luks_store_header_identity(new_home, setup.crypt_device, setup.volume_key, header_home); + if (r < 0) + return r; + + r = store_embedded_identity(new_home, setup.root_fd, h->uid, embedded_home); + if (r < 0) + return r; + + r = extend_embedded_identity(new_home, h, &setup); + if (r < 0) + return r; + + r = sync_and_statfs(setup.root_fd, NULL); + if (r < 0) + return r; + + r = home_setup_undo(&setup); + if (r < 0) + return r; + + log_info("Everything completed."); + + *ret = TAKE_PTR(new_home); + return 0; +} + +enum { + CAN_RESIZE_ONLINE, + CAN_RESIZE_OFFLINE, +}; + +static int can_resize_fs(int fd, uint64_t old_size, uint64_t new_size) { + struct statfs sfs; + + assert(fd >= 0); + + /* Filter out bogus requests early */ + if (old_size == 0 || old_size == UINT64_MAX || + new_size == 0 || new_size == UINT64_MAX) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid resize parameters."); + + if ((old_size & 511) != 0 || (new_size & 511) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Resize parameters not multiple of 512."); + + if (fstatfs(fd, &sfs) < 0) + return log_error_errno(errno, "Failed to fstatfs() file system: %m"); + + if (is_fs_type(&sfs, BTRFS_SUPER_MAGIC)) { + + if (new_size < BTRFS_MINIMAL_SIZE) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "New file system size too small for btrfs (needs to be 256M at least."); + + /* btrfs can grow and shrink online */ + + } else if (is_fs_type(&sfs, XFS_SB_MAGIC)) { + + if (new_size < XFS_MINIMAL_SIZE) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "New file system size too small for xfs (needs to be 14M at least)."); + + /* XFS can grow, but not shrink */ + if (new_size < old_size) + return log_error_errno(SYNTHETIC_ERRNO(EMSGSIZE), "Shrinking this type of file system is not supported."); + + } else if (is_fs_type(&sfs, EXT4_SUPER_MAGIC)) { + + if (new_size < EXT4_MINIMAL_SIZE) + return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "New file system size too small for ext4 (needs to be 1M at least)."); + + /* ext4 can grow online, and shrink offline */ + if (new_size < old_size) + return CAN_RESIZE_OFFLINE; + + } else + return log_error_errno(SYNTHETIC_ERRNO(ESOCKTNOSUPPORT), "Resizing this type of file system is not supported."); + + return CAN_RESIZE_ONLINE; +} + +static int ext4_offline_resize_fs(HomeSetup *setup, uint64_t new_size, bool discard) { + _cleanup_free_ char *size_str = NULL; + bool re_open = false, re_mount = false; + pid_t resize_pid, fsck_pid; + int r, exit_status; + + assert(setup); + assert(setup->dm_node); + + /* First, unmount the file system */ + if (setup->root_fd >= 0) { + setup->root_fd = safe_close(setup->root_fd); + re_open = true; + } + + if (setup->undo_mount) { + r = umount_verbose("/run/systemd/user-home-mount"); + if (r < 0) + return r; + + setup->undo_mount = false; + re_mount = true; + } + + log_info("Temporarary unmounting of file system completed."); + + /* resize2fs requires that the file system is force checked first, do so. */ + r = safe_fork("(e2fsck)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_STDOUT_TO_STDERR, &fsck_pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + execlp("e2fsck" ,"e2fsck", "-fp", setup->dm_node, NULL); + log_error_errno(errno, "Failed to execute e2fsck: %m"); + _exit(EXIT_FAILURE); + } + + exit_status = wait_for_terminate_and_check("e2fsck", fsck_pid, WAIT_LOG_ABNORMAL); + if (exit_status < 0) + return exit_status; + if ((exit_status & ~FSCK_ERROR_CORRECTED) != 0) { + log_warning("e2fsck failed with exit status %i.", exit_status); + + if ((exit_status & (FSCK_SYSTEM_SHOULD_REBOOT|FSCK_ERRORS_LEFT_UNCORRECTED)) != 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "File system is corrupted, refusing."); + + log_warning("Ignoring fsck error."); + } + + log_info("Forced file system check completed."); + + /* We use 512 sectors here, because resize2fs doesn't do byte sizes */ + if (asprintf(&size_str, "%" PRIu64 "s", new_size / 512) < 0) + return log_oom(); + + /* Resize the thing */ + r = safe_fork("(e2resize)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_DEATHSIG|FORK_LOG|FORK_WAIT|FORK_STDOUT_TO_STDERR, &resize_pid); + if (r < 0) + return r; + if (r == 0) { + /* Child */ + execlp("resize2fs" ,"resize2fs", setup->dm_node, size_str, NULL); + log_error_errno(errno, "Failed to execute resize2fs: %m"); + _exit(EXIT_FAILURE); + } + + log_info("Offline file system resize completed."); + + /* Re-establish mounts and reopen the directory */ + if (re_mount) { + r = mount_node(setup->dm_node, "ext4", discard); + if (r < 0) + return r; + + setup->undo_mount = true; + } + + if (re_open) { + setup->root_fd = open("/run/systemd/user-home-mount", O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (setup->root_fd < 0) + return log_error_errno(errno, "Failed to reopen file system: %m"); + } + + log_info("File system mounted again."); + + return 0; +} + +static int prepare_resize_partition( + int fd, + uint64_t partition_offset, + uint64_t old_partition_size, + uint64_t new_partition_size, + sd_id128_t *ret_disk_uuid, + struct fdisk_table **ret_table) { + + _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL; + _cleanup_(fdisk_unref_tablep) struct fdisk_table *t = NULL; + _cleanup_free_ char *path = NULL, *disk_uuid_as_string = NULL; + size_t n_partitions, i; + sd_id128_t disk_uuid; + bool found = false; + int r; + + assert(fd >= 0); + assert(ret_disk_uuid); + assert(ret_table); + + assert((partition_offset & 511) == 0); + assert((old_partition_size & 511) == 0); + assert((new_partition_size & 511) == 0); + assert(UINT64_MAX - old_partition_size >= partition_offset); + assert(UINT64_MAX - new_partition_size >= partition_offset); + + if (partition_offset == 0) { + /* If the offset is at the beginning we assume no partition table, let's exit early. */ + log_debug("Not rewriting partition table, operating on naked device."); + *ret_disk_uuid = SD_ID128_NULL; + *ret_table = NULL; + return 0; + } + + c = fdisk_new_context(); + if (!c) + return log_oom(); + + if (asprintf(&path, "/proc/self/fd/%i", fd) < 0) + return log_oom(); + + r = fdisk_assign_device(c, path, 0); + if (r < 0) + return log_error_errno(r, "Failed to open device: %m"); + + if (!fdisk_is_labeltype(c, FDISK_DISKLABEL_GPT)) + return log_error_errno(SYNTHETIC_ERRNO(ENOMEDIUM), "Disk has no GPT partition table."); + + r = fdisk_get_disklabel_id(c, &disk_uuid_as_string); + if (r < 0) + return log_error_errno(r, "Failed to acquire disk UUID: %m"); + + r = sd_id128_from_string(disk_uuid_as_string, &disk_uuid); + if (r < 0) + return log_error_errno(r, "Failed parse disk UUID: %m"); + + r = fdisk_get_partitions(c, &t); + if (r < 0) + return log_error_errno(r, "Failed to acquire partition table: %m"); + + n_partitions = fdisk_table_get_nents(t); + for (i = 0; i < n_partitions; i++) { + struct fdisk_partition *p; + + p = fdisk_table_get_partition(t, i); + if (r < 0) + return log_error_errno(r, "Failed to read partition metadata: %m"); + + if (fdisk_partition_is_used(p) <= 0) + continue; + if (fdisk_partition_has_start(p) <= 0 || fdisk_partition_has_size(p) <= 0 || fdisk_partition_has_end(p) <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Found partition without a size."); + + if (fdisk_partition_get_start(p) == partition_offset / 512U && + fdisk_partition_get_size(p) == old_partition_size / 512U) { + + if (found) + return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Partition found twice, refusing."); + + /* Found our partition, now patch it */ + r = fdisk_partition_size_explicit(p, 1); + if (r < 0) + return log_error_errno(r, "Failed to enable explicit partition size: %m"); + + r = fdisk_partition_set_size(p, new_partition_size / 512U); + if (r < 0) + return log_error_errno(r, "Failed to change partition size: %m"); + + found = true; + continue; + + } else { + if (fdisk_partition_get_start(p) < partition_offset + new_partition_size / 512U && + fdisk_partition_get_end(p) >= partition_offset / 512) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Can't extend, conflicting partition found."); + } + } + + if (!found) + return log_error_errno(SYNTHETIC_ERRNO(ENOPKG), "Failed to find matching partition to resize."); + + *ret_table = TAKE_PTR(t); + *ret_disk_uuid = disk_uuid; + + return 1; +} + +static int ask_cb(struct fdisk_context *c, struct fdisk_ask *ask, void *userdata) { + char *result; + + assert(c); + + switch (fdisk_ask_get_type(ask)) { + + case FDISK_ASKTYPE_STRING: + result = new(char, 37); + if (!result) + return log_oom(); + + fdisk_ask_string_set_result(ask, id128_to_uuid_string(*(sd_id128_t*) userdata, result)); + break; + + default: + log_debug("Unexpected question from libfdisk, ignoring."); + } + + return 0; +} + +static int apply_resize_partition(int fd, sd_id128_t disk_uuids, struct fdisk_table *t) { + _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL; + _cleanup_free_ void *two_zero_lbas = NULL; + _cleanup_free_ char *path = NULL; + ssize_t n; + int r; + + assert(fd >= 0); + + if (!t) /* no partition table to apply, exit early */ + return 0; + + two_zero_lbas = malloc0(1024U); + if (!two_zero_lbas) + return log_oom(); + + /* libfdisk appears to get confused by the existing PMBR. Let's explicitly flush it out. */ + n = pwrite(fd, two_zero_lbas, 1024U, 0); + if (n < 0) + return log_error_errno(r, "Failed to wipe partition table: %m"); + if (n != 1024) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short write while whiping partition table."); + + c = fdisk_new_context(); + if (!c) + return log_oom(); + + if (asprintf(&path, "/proc/self/fd/%i", fd) < 0) + return log_oom(); + + r = fdisk_assign_device(c, path, 0); + if (r < 0) + return log_error_errno(r, "Failed to open device: %m"); + + r = fdisk_create_disklabel(c, "gpt"); + if (r < 0) + return log_error_errno(r, "Failed to create GPT disk label: %m"); + + r = fdisk_apply_table(c, t); + if (r < 0) + return log_error_errno(r, "Failed to apply partition table: %m"); + + r = fdisk_set_ask(c, ask_cb, &disk_uuids); + if (r < 0) + return log_error_errno(r, "Failed to set libfdisk query function: %m"); + + r = fdisk_set_disklabel_id(c); + if (r < 0) + return log_error_errno(r, "Failed to change disklabel ID: %m"); + + r = fdisk_write_disklabel(c); + if (r < 0) + return log_error_errno(r, "Failed to write disk label: %m"); + + return 1; +} + +static int home_resize_luks(UserRecord *h, bool already_activated, HomeSetup *setup, UserRecord **ret_home) { + char buffer1[FORMAT_BYTES_MAX], buffer2[FORMAT_BYTES_MAX], buffer3[FORMAT_BYTES_MAX], + buffer4[FORMAT_BYTES_MAX], buffer5[FORMAT_BYTES_MAX], buffer6[FORMAT_BYTES_MAX]; + uint64_t old_image_size, new_image_size, old_fs_size, new_fs_size, crypto_offset, new_partition_size; + _cleanup_(user_record_unrefp) UserRecord *header_home = NULL, *embedded_home = NULL, *new_home = NULL; + _cleanup_(fdisk_unref_tablep) struct fdisk_table *table = NULL; + _cleanup_free_ char *whole_disk = NULL; + _cleanup_close_ int image_fd = -1; + sd_id128_t disk_uuid; + const char *ip, *ipo; + struct statfs sfs; + struct stat st; + int r, resize_type; + + assert(h); + assert(user_record_storage(h) == USER_LUKS); + assert(setup); + assert(ret_home); + + assert_se(ipo = user_record_image_path(h)); + ip = strdupa(ipo); /* copy out since original might change later in home record object */ + + image_fd = open(ip, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (image_fd < 0) + return log_error_errno(errno, "Failed to open image file %s: %m", ip); + + if (fstat(image_fd, &st) < 0) + return log_error_errno(errno, "Failed to stat image file %s: %m", ip); + if (S_ISBLK(st.st_mode)) { + dev_t parent; + + r = block_get_whole_disk(st.st_rdev, &parent); + if (r < 0) + return log_error_errno(r, "Failed to acquire whole block device for %s: %m", ip); + if (r > 0) { + /* If we shall resize a file system on a partition device, then let's figure out the + * whole disk device and operate on that instead, since we need to rewrite the + * partition table to resize the partition. */ + + log_info("Operating on partition device %s, using parent device.", ip); + + r = device_path_make_major_minor(st.st_mode, parent, &whole_disk); + if (r < 0) + return log_error_errno(r, "Failed to derive whole disk path for %s: %m", ip); + + safe_close(image_fd); + + image_fd = open(whole_disk, O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); + if (image_fd < 0) + return log_error_errno(errno, "Failed to open whole block device %s: %m", whole_disk); + + if (fstat(image_fd, &st) < 0) + return log_error_errno(errno, "Failed to stat whole block device %s: %m", whole_disk); + if (!S_ISBLK(st.st_mode)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Whole block device %s is not actually a block device, refusing.", whole_disk); + } else + log_info("Operating on whole block device %s.", ip); + + if (ioctl(image_fd, BLKGETSIZE64, &old_image_size) < 0) + return log_error_errno(errno, "Failed to determine size of original block device: %m"); + + if (flock(image_fd, LOCK_EX) < 0) /* make sure udev doesn't read from it while we operate on the device */ + return log_error_errno(errno, "Failed to lock block device %s: %m", ip); + + new_image_size = old_image_size; /* we can't resize physical block devices */ + } else { + r = stat_verify_regular(&st); + if (r < 0) + return log_error_errno(r, "Image file %s is not a block device nor regular: %m", ip); + + old_image_size = st.st_size; + + /* Note an asymetry here: when we operate on loopback files the specified disk size we get we + * apply onto the loopback file as a whole. When we operate on block devices we instead apply + * to the partition itself only. */ + + new_image_size = DISK_SIZE_ROUND_DOWN(h->disk_size); + if (new_image_size == old_image_size) { + log_info("Image size already matching, skipping operation."); + return 0; + } + } + + r = home_prepare_luks(h, already_activated, whole_disk, setup, &header_home); + if (r < 0) + return r; + + r = load_embedded_identity(h, setup->root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, &embedded_home, &new_home); + if (r < 0) + return r; + + log_info("offset = %" PRIu64 ", size = %" PRIu64 ", image = %" PRIu64, setup->partition_offset, setup->partition_size, old_image_size); + + if ((UINT64_MAX - setup->partition_offset) < setup->partition_size || + setup->partition_offset + setup->partition_size > old_image_size) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Old partition doesn't fit in backing storage, refusing."); + + if (S_ISREG(st.st_mode)) { + uint64_t partition_table_extra; + + partition_table_extra = old_image_size - setup->partition_size; + if (new_image_size <= partition_table_extra) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "New size smaller than partition table metadata."); + + new_partition_size = new_image_size - partition_table_extra; + } else { + assert(S_ISBLK(st.st_mode)); + + new_partition_size = DISK_SIZE_ROUND_DOWN(h->disk_size); + if (new_partition_size == setup->partition_size) { + log_info("Partition size already matching, skipping operation."); + return 0; + } + } + + if ((UINT64_MAX - setup->partition_offset) < new_partition_size || + setup->partition_offset + new_partition_size > new_image_size) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "New partition doesn't fit into backing storage, refusing."); + + crypto_offset = crypt_get_data_offset(setup->crypt_device); + if (setup->partition_size / 512U <= crypto_offset) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Weird, old crypto payload offset doesn't actually fit in partition size?"); + if (new_partition_size / 512U <= crypto_offset) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "New size smaller than crypto payload offset?"); + + old_fs_size = (setup->partition_size / 512U - crypto_offset) * 512U; + new_fs_size = (new_partition_size / 512U - crypto_offset) * 512U; + + /* Before we start doing anything, let's figure out if we actually can */ + resize_type = can_resize_fs(setup->root_fd, old_fs_size, new_fs_size); + if (resize_type < 0) + return resize_type; + if (resize_type == CAN_RESIZE_OFFLINE && already_activated) + return log_error_errno(SYNTHETIC_ERRNO(ETXTBSY), "File systems of this type can only be resized offline, but is currently online."); + + log_info("Ready to resize image size %s → %s, partition size %s → %s, file system size %s → %s.", + format_bytes(buffer1, sizeof(buffer1), old_image_size), + format_bytes(buffer2, sizeof(buffer2), new_image_size), + format_bytes(buffer3, sizeof(buffer3), setup->partition_size), + format_bytes(buffer4, sizeof(buffer4), new_partition_size), + format_bytes(buffer5, sizeof(buffer5), old_fs_size), + format_bytes(buffer6, sizeof(buffer6), new_fs_size)); + + r = prepare_resize_partition( + image_fd, + setup->partition_offset, + setup->partition_size, + new_partition_size, + &disk_uuid, + &table); + if (r < 0) + return r; + + if (new_fs_size > old_fs_size) { + + if (S_ISREG(st.st_mode)) { + /* Grow file size */ + + if (user_record_luks_discard(h)) + r = ftruncate(image_fd, new_image_size); + else + r = fallocate(image_fd, 0, 0, new_image_size); + if (r < 0) { + if (ERRNO_IS_DISK_SPACE(errno)) { + log_debug_errno(errno, "Not enough disk space to grow home."); + return -ENOSPC; /* make recognizable */ + } + + return log_error_errno(errno, "Failed to grow image file %s: %m", ip); + } + + log_info("Growing of image file completed."); + } + + /* Make sure loopback device sees the new bigger size */ + r = loop_device_refresh_size(setup->loop, UINT64_MAX, new_partition_size); + if (r == -ENOTTY) + log_debug_errno(r, "Device is not a loopback device, not refreshing size."); + else if (r < 0) + return log_error_errno(r, "Failed to refresh loopback device size: %m"); + else + log_info("Refreshing loop device size completed."); + + r = apply_resize_partition(image_fd, disk_uuid, table); + if (r < 0) + return r; + if (r > 0) + log_info("Growing of partition completed."); + + if (ioctl(image_fd, BLKRRPART, 0) < 0) + log_debug_errno(errno, "BLKRRPART failed on block device, ignoring: %m"); + + /* Tell LUKS about the new bigger size too */ + r = crypt_resize(setup->crypt_device, setup->dm_name, new_fs_size / 512U); + if (r < 0) + return log_error_errno(r, "Failed to grow LUKS device: %m"); + + log_info("LUKS device growing completed."); + } else { + r = store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home); + if (r < 0) + return r; + + if (S_ISREG(st.st_mode)) { + if (user_record_luks_discard(h)) + /* Before we shrink, let's trim the file system, so that we need less space on disk during the shrinking */ + (void) run_fitrim(setup->root_fd); + else { + /* If discard is off, let's ensure all backing blocks are allocated, so that our resize operation doesn't fail half-way */ + r = run_fallocate(image_fd, &st); + if (r < 0) + return r; + } + } + } + + /* Now resize the file system */ + if (resize_type == CAN_RESIZE_ONLINE) + r = resize_fs(setup->root_fd, new_fs_size); + else + r = ext4_offline_resize_fs(setup, new_fs_size, user_record_luks_discard(h)); + if (r < 0) + return log_error_errno(r, "Failed to resize file system: %m"); + + log_info("File system resizing completed."); + + /* Immediately sync afterwards */ + r = sync_and_statfs(setup->root_fd, NULL); + if (r < 0) + return r; + + if (new_fs_size < old_fs_size) { + + /* Shrink the LUKS device now, matching the new file system size */ + r = crypt_resize(setup->crypt_device, setup->dm_name, new_fs_size / 512); + if (r < 0) + return log_error_errno(r, "Failed to shrink LUKS device: %m"); + + log_info("LUKS device shrinking completed."); + + if (S_ISREG(st.st_mode)) { + /* Shrink the image file */ + if (ftruncate(image_fd, new_image_size) < 0) + return log_error_errno(errno, "Failed to shrink image file %s: %m", ip); + + log_info("Shrinking of image file completed."); + } + + /* Refresh the loop devices size */ + r = loop_device_refresh_size(setup->loop, UINT64_MAX, new_partition_size); + if (r == -ENOTTY) + log_debug_errno(r, "Device is not a loopback device, not refreshing size."); + else if (r < 0) + return log_error_errno(r, "Failed to refresh loopback device size: %m"); + else + log_info("Refreshing loop device size completed."); + + r = apply_resize_partition(image_fd, disk_uuid, table); + if (r < 0) + return r; + if (r > 0) + log_info("Shrinking of partition completed."); + + if (ioctl(image_fd, BLKRRPART, 0) < 0) + log_debug_errno(errno, "BLKRRPART failed on block device, ignoring: %m"); + } else { + r = store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home); + if (r < 0) + return r; + } + + r = luks_store_header_identity(new_home, setup->crypt_device, setup->volume_key, header_home); + if (r < 0) + return r; + + r = extend_embedded_identity(new_home, h, setup); + if (r < 0) + return r; + + if (user_record_luks_discard(h)) + (void) run_fitrim(setup->root_fd); + + r = sync_and_statfs(setup->root_fd, &sfs); + if (r < 0) + return r; + + r = home_setup_undo(setup); + if (r < 0) + return r; + + log_info("Everything completed."); + + print_size_summary(new_image_size, new_fs_size, &sfs); + + *ret_home = TAKE_PTR(new_home); + return 0; +} + +static int home_resize_directory(UserRecord *h, bool already_activated, HomeSetup *setup, UserRecord **ret_home) { + _cleanup_(user_record_unrefp) UserRecord *embedded_home = NULL, *new_home = NULL; + int r; + + assert(h); + assert(setup); + assert(ret_home); + assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT)); + + r = home_prepare(h, already_activated, setup, NULL); + if (r < 0) + return r; + + r = load_embedded_identity(h, setup->root_fd, NULL, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, &embedded_home, &new_home); + if (r < 0) + return r; + + r = home_update_quota_auto(h, NULL); + if (ERRNO_IS_NOT_SUPPORTED(r)) + return -ESOCKTNOSUPPORT; /* make recognizable */ + if (r < 0) + return r; + + r = store_embedded_identity(new_home, setup->root_fd, h->uid, embedded_home); + if (r < 0) + return r; + + r = extend_embedded_identity(new_home, h, setup); + if (r < 0) + return r; + + r = sync_and_statfs(setup->root_fd, NULL); + if (r < 0) + return r; + + r = home_setup_undo(setup); + if (r < 0) + return r; + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 0; +} + +static int home_resize(UserRecord *h, UserRecord **ret) { + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + bool already_activated = false; + int r; + + assert(h); + assert(ret); + + if (h->disk_size == UINT64_MAX) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No target size specified, refusing."); + + r = user_record_validate_hashed_password(h); + if (r < 0) + return r; + + r = home_validate_update(h, &setup); + if (r < 0) + return r; + + already_activated = r > 0; + + switch (user_record_storage(h)) { + + case USER_LUKS: + return home_resize_luks(h, already_activated, &setup, ret); + + case USER_DIRECTORY: + case USER_SUBVOLUME: + case USER_FSCRYPT: + return home_resize_directory(h, already_activated, &setup, ret); + + default: + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Resizing home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + } +} + +static int luks_passwd(struct crypt_device *cd, char **passwords, char **hashed_passwords) { + _cleanup_free_ char **add = NULL; /* Note: do not use strv_free() here! */ + size_t volume_key_size, added = 0, i, max_key_slots; + _cleanup_(erase_and_freep) void *volume_key = NULL; + bool volume_key_loaded = false; + const char *type; + char **pp; + int r; + + if (!cd) + return 0; + + type = crypt_get_type(cd); + if (!type) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine crypto device type."); + + r = crypt_keyslot_max(type); + if (r <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine number of key slots."); + max_key_slots = r; + + r = crypt_get_volume_key_size(cd); + if (r <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine volume key size."); + volume_key_size = r; + + volume_key = malloc(volume_key_size); + if (!volume_key) + return log_oom(); + + add = new(char*, strv_length(passwords)); + if (!add) + return log_oom(); + + STRV_FOREACH(pp, passwords) { + /* Look if this password is among the hashed passwords. If so, it's a new password to set, + * otherwise an old password to use to unlock */ + + r = test_password(hashed_passwords, *pp); + if (r < 0) + return r; + if (r > 0) + /* This is a new key to add */ + add[added++] = *pp; + + if (!volume_key_loaded) { + /* Try to load the volume key with this */ + r = crypt_volume_key_get(cd, CRYPT_ANY_SLOT, volume_key, &volume_key_size, *pp, strlen(*pp)); + if (r == -EPERM) { + log_debug_errno(r, "Password %zu didn't work.", (size_t) (pp - passwords)); + continue; + } + if (r < 0) + return log_error_errno(r, "Failed to unlock LUKS superblock: %m"); + + log_info("Unlocked LUKS superblock with key %zu.", (size_t) (pp - passwords)); + volume_key_loaded = true; + } + } + + if (added == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Found no new key to set, refusing."); + + if (!volume_key_loaded) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Failed to unlock LUKS superblock with all keys."); + + for (i = 0; i < max_key_slots; i++) { + r = crypt_keyslot_destroy(cd, i); + if (r < 0 && !IN_SET(r, -ENOENT, -EINVAL)) /* Returns EINVAL or ENOENT if there's no key in this slot already */ + return log_error_errno(r, "Failed to destroy LUKS password: %m"); + + if (i >= added) { + if (r >= 0) + log_info("Destroyed LUKS key slot %zu.", i); + } else { + r = crypt_keyslot_add_by_volume_key( + cd, + i, + volume_key, + volume_key_size, + add[i], + strlen(add[i])); + if (r < 0) + return log_error_errno(r, "Failed to set up LUKS password: %m"); + + log_info("Updated LUKS key slot %zu.", i); + } + } + + return 1; +} + +static int home_passwd(UserRecord *h, UserRecord **ret_home) { + _cleanup_(user_record_unrefp) UserRecord *header_home = NULL, *embedded_home = NULL, *new_home = NULL; + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + bool already_activated = false; + int r; + + assert(h); + assert(ret_home); + + /* Yes that's right, we cannot change passwords for "fscrypt" directories right now, since ext4 + * doesn't support re-encryption. We could add that by not deriving the master key directly from the + * password, but then we'd need some side storage (xattr?) and would be in the crypto business a + * bit too much for my taste (also home directories would not be self-contained anymore + * then). Alternatively we could do reencrypt in userspace, by setting up a new directory and copying + * things over. But that'd be terribly slow. */ + if (!IN_SET(user_record_storage(h), USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME)) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Changing password of home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + r = user_record_validate_hashed_password(h); + if (r < 0) + return r; + + r = home_validate_update(h, &setup); + if (r < 0) + return r; + + already_activated = r > 0; + + r = home_prepare(h, already_activated, &setup, &header_home); + if (r < 0) + return r; + + r = load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, &embedded_home, &new_home); + if (r < 0) + return r; + + r = luks_passwd(setup.crypt_device, h->password, h->hashed_password); + if (r < 0) + return r; + + r = luks_store_header_identity(new_home, setup.crypt_device, setup.volume_key, header_home); + if (r < 0) + return r; + + r = store_embedded_identity(new_home, setup.root_fd, h->uid, embedded_home); + if (r < 0) + return r; + + r = extend_embedded_identity(new_home, h, &setup); + if (r < 0) + return r; + + r = sync_and_statfs(setup.root_fd, NULL); + if (r < 0) + return r; + + r = home_setup_undo(&setup); + if (r < 0) + return r; + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 1; +} + +static int home_inspect(UserRecord *h, UserRecord **ret_home) { + _cleanup_(home_setup_undo) HomeSetup setup = HOME_SETUP_INIT; + _cleanup_(user_record_unrefp) UserRecord *header_home = NULL, *embedded_home = NULL, *new_home = NULL; + bool already_activated = false; + int r; + + assert(h); + assert(ret_home); + + r = user_record_validate_hashed_password(h); + if (r < 0) + return r; + + r = home_validate_update(h, &setup); + if (r < 0) + return r; + + already_activated = r > 0; + + r = home_prepare(h, already_activated, &setup, &header_home); + if (r < 0) + return r; + + r = load_embedded_identity(h, setup.root_fd, header_home, USER_RECONCILE_ANY, NULL, &new_home); + if (r < 0) + return r; + + r = extend_embedded_identity(new_home, h, &setup); + if (r < 0) + return r; + + r = home_setup_undo(&setup); + if (r < 0) + return r; + + log_info("Everything completed."); + + *ret_home = TAKE_PTR(new_home); + return 1; +} + +static int home_lock(UserRecord *h) { + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_free_ char *dm_name = NULL, *dm_node = NULL; + _cleanup_close_ int root_fd = -1; + const char *p; + int r; + + assert(h); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record incomplete, refusing."); + if (user_record_storage(h) != USER_LUKS) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Locking home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + r = user_record_test_home_directory_and_warn(h); + if (r < 0) + return r; + if (r != USER_TEST_MOUNTED) + return log_error_errno(SYNTHETIC_ERRNO(ENOEXEC), "Home directory of %s is not mounted, can't lock.", h->user_name); + + assert_se(p = user_record_home_directory(h)); + root_fd = open(p, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) + return log_error_errno(errno, "Failed to open home directory: %m"); + + r = make_dm_names(h->user_name, &dm_name, &dm_node); + if (r < 0) + return r; + + r = crypt_init_by_name(&cd, dm_name); + if (r < 0) + return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name); + + log_info("Discovered used LUKS device %s.", dm_node); + crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); + + if (syncfs(root_fd) < 0) /* Snake oil, but let's better be safe than sorry */ + return log_error_errno(errno, "Failed to synchronize file system %s: %m", p); + + root_fd = safe_close(root_fd); + + log_info("File system synchronized."); + + /* Note that we don't invoke FIFREEZE here, it appears libcryptsetup/device-mapper already does that on its own for us */ + + r = crypt_suspend(cd, dm_name); + if (r < 0) { + /* (void) ioctl(root_fd, FITHAW, 0); */ + return log_error_errno(r, "Failed to suspend cryptsetup device: %s: %m", dm_node); + } + + log_info("LUKS device suspended."); + + log_info("Everything completed."); + return 1; +} + +static int home_unlock(UserRecord *h) { + _cleanup_(crypt_freep) struct crypt_device *cd = NULL; + _cleanup_free_ char *dm_name = NULL, *dm_node = NULL; + bool resumed = false; + char **pp; + int r; + + assert(h); + + if (!h->user_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "User record incomplete, refusing."); + if (user_record_storage(h) != USER_LUKS) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), "Unlocking home directories of type '%s' currently not supported.", user_storage_to_string(user_record_storage(h))); + + r = user_record_validate_hashed_password(h); + if (r < 0) + return r; + + /* Note that we don't check if $HOME is actually mounted, since we want to avoid disk accesses on that mount until we have resumed the device. */ + + r = make_dm_names(h->user_name, &dm_name, &dm_node); + if (r < 0) + return r; + + r = crypt_init_by_name(&cd, dm_name); + if (r < 0) + return log_error_errno(r, "Failed to initialize cryptsetup context for %s: %m", dm_name); + + log_info("Discovered used LUKS device %s.", dm_node); + crypt_set_log_callback(cd, cryptsetup_log_glue, NULL); + + STRV_FOREACH(pp, h->password) { + r = crypt_resume_by_passphrase(cd, dm_name, CRYPT_ANY_SLOT, *pp, strlen(*pp)); + if (r < 0) + log_debug_errno(r, "Password %zu didn't work for resuming device: %m", (size_t) (pp - h->password)); + else { + log_info("Resumed LUKS device %s.", dm_node); + resumed = true; + break; + } + } + + if (!resumed) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "No valid key for LUKS superblock."); + + log_info("LUKS device resumed."); + + log_info("Everything completed."); + return 1; +} + +static int run(int argc, char *argv[]) { + _cleanup_(user_record_unrefp) UserRecord *home = NULL, *new_home = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(fclosep) FILE *opened_file = NULL; + unsigned line = 0, column = 0; + const char *json_path = NULL; + FILE *json_file; + usec_t start; + int r; + + start = now(CLOCK_MONOTONIC); + + log_setup_service(); + + umask(0022); + + if (argc < 2 || argc > 3) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes one or two arguments."); + + if (argc > 2) { + json_path = argv[2]; + + opened_file = fopen(json_path, "re"); + if (!opened_file) + return log_error_errno(errno, "Failed to open %s: %m", json_path); + + json_file = opened_file; + } else { + json_path = ""; + json_file = stdin; + } + + r = json_parse_file(json_file, json_path, JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "[%s:%u:%u] Failed to parse JSON data: %m", json_path, line, column); + + home = user_record_new(); + if (!home) + return log_oom(); + + r = user_record_load(home, v, USER_RECORD_LOAD_FULL|USER_RECORD_LOG); + if (r < 0) + return r; + + /* Well known return values of these operations, that systemd-homed knows and converts to proper D-Bus errors: + * + * EMSGSIZE → file systems of this type cannnot be shrinked + * ETXTBSY → file systems of this type can only be shrinked offline + * ERANGE → file system size too small + * ENOLINK → system does not support selected storage backend + * EPROTONOSUPPORT → system does not support selected file system + * ENOTTY → operation not support on this storage + * ESOCKTNOSUPPORT → operation not support on this file system + * ENOKEY → password incorrect (or not sufficient) + * EBUSY → file system is currently active + * ENOEXEC → file system is currently not active + * ENOSPC → not enough disk space for operation + */ + + if (streq(argv[1], "activate")) + r = home_activate(home, &new_home); + else if (streq(argv[1], "deactivate")) + r = home_deactivate(home, false); + else if (streq(argv[1], "deactivate-force")) + r = home_deactivate(home, true); + else if (streq(argv[1], "create")) + r = home_create(home, &new_home); + else if (streq(argv[1], "remove")) + r = home_remove(home); + else if (streq(argv[1], "update")) + r = home_update(home, &new_home); + else if (streq(argv[1], "resize")) + r = home_resize(home, &new_home); + else if (streq(argv[1], "passwd")) + r = home_passwd(home, &new_home); + else if (streq(argv[1], "inspect")) + r = home_inspect(home, &new_home); + else if (streq(argv[1], "lock")) + r = home_lock(home); + else if (streq(argv[1], "unlock")) + r = home_unlock(home); + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown verb '%s'.", argv[1]); + if (r == -ENOKEY) { + usec_t end, n, d; + + /* Make sure bad passwords replies always take at least 3s, and if longer multiples of 3s, so + * that it's not clear how long we actually needed for our calculations. */ + n = now(CLOCK_MONOTONIC); + assert(n >= start); + + d = usec_sub_unsigned(n, start); + if (d > BAD_PASSWORD_DELAY_USEC) + end = start + DIV_ROUND_UP(d, BAD_PASSWORD_DELAY_USEC) * BAD_PASSWORD_DELAY_USEC; + else + end = start + BAD_PASSWORD_DELAY_USEC; + + if (n < end) + (void) usleep(usec_sub_unsigned(end, n)); + } + if (r < 0) + return r; + + /* We always pass the new record back, regardless if it changed or not. This allows our caller to + * prepare a fresh record, send to us, and only if it works use it without having to keep a local + * copy. */ + if (new_home) + json_variant_dump(new_home->json, JSON_FORMAT_NEWLINE, stdout, NULL); + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/home/meson.build b/src/home/meson.build new file mode 100644 index 0000000000000..33bf36345d4fb --- /dev/null +++ b/src/home/meson.build @@ -0,0 +1,63 @@ +# SPDX-License-Identifier: LGPL-2.1+ + +systemd_homework_sources = files(''' + home-util.c + home-util.h + homework.c + user-record-util.c + user-record-util.h +'''.split()) + +systemd_homed_sources = files(''' + home-util.c + home-util.h + homed-bus.c + homed-bus.h + homed-home-bus.c + homed-home-bus.h + homed-home.c + homed-home.h + homed-manager-bus.c + homed-manager-bus.h + homed-manager.c + homed-manager.h + homed-operation.c + homed-operation.h + homed-varlink.c + homed-varlink.h + homed.c + pwquality-util.c + pwquality-util.h + user-record-sign.c + user-record-sign.h + user-record-util.c + user-record-util.h +'''.split()) + +homectl_sources = files(''' + home-util.c + home-util.h + homectl.c + pwquality-util.c + pwquality-util.h + user-record-util.c + user-record-util.h +'''.split()) + +pam_systemd_home_sym = 'src/home/pam_systemd_home.sym' +pam_systemd_home_c = files(''' + home-util.c + home-util.h + pam_systemd_home.c + user-record-util.c + user-record-util.h +'''.split()) + +if conf.get('ENABLE_HOMED') == 1 + install_data('org.freedesktop.home1.conf', + install_dir : dbuspolicydir) + install_data('org.freedesktop.home1.service', + install_dir : dbussystemservicedir) + install_data('org.freedesktop.home1.policy', + install_dir : polkitpolicydir) +endif diff --git a/src/home/org.freedesktop.home1.conf b/src/home/org.freedesktop.home1.conf new file mode 100644 index 0000000000000..d615501054d74 --- /dev/null +++ b/src/home/org.freedesktop.home1.conf @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/home/org.freedesktop.home1.policy b/src/home/org.freedesktop.home1.policy new file mode 100644 index 0000000000000..66ef8e0e9d45c --- /dev/null +++ b/src/home/org.freedesktop.home1.policy @@ -0,0 +1,72 @@ + + + + + + + + The systemd Project + http://www.freedesktop.org/wiki/Software/systemd + + + Create a home + Authentication is required for creating a user's home. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + + Remove a home + Authentication is required for removing a user's home. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + + Check credentials of a home + Authentication is required for checking credentials against a user's home. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + + Update a home + Authentication is required for updating a user's home. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + + Resize a home + Authentication is required for resizing a user's home. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + + Change password of a home + Authentication is required for changing the password of a user's home. + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + + diff --git a/src/home/org.freedesktop.home1.service b/src/home/org.freedesktop.home1.service new file mode 100644 index 0000000000000..cff19b38617df --- /dev/null +++ b/src/home/org.freedesktop.home1.service @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1+ + +[D-BUS Service] +Name=org.freedesktop.home1 +Exec=/bin/false +User=root +SystemdService=dbus-org.freedesktop.home1.service diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c new file mode 100644 index 0000000000000..6f795ecb96103 --- /dev/null +++ b/src/home/pam_systemd_home.c @@ -0,0 +1,836 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include + +#include "sd-bus.h" + +#include "bus-common-errors.h" +#include "errno-util.h" +#include "fd-util.h" +#include "home-util.h" +#include "memory-util.h" +#include "pam-util.h" +#include "parse-util.h" +#include "strv.h" +#include "user-record-util.h" +#include "user-record.h" + +/* Used for the "systemd-user-record-is-homed" PAM data field, to indicate whether we know whether this user + * record is managed by homed or by something else. */ +#define USER_RECORD_IS_HOMED INT_TO_PTR(1) +#define USER_RECORD_IS_OTHER INT_TO_PTR(2) + +static int parse_argv( + pam_handle_t *handle, + int argc, const char **argv, + bool *please_suspend, + bool *debug) { + + int i; + + assert(argc >= 0); + assert(argc == 0 || argv); + + for (i = 0; i < argc; i++) { + const char *v; + + if ((v = startswith(argv[1], "suspend="))) { + int k; + + k = parse_boolean(v); + if (k < 0) + pam_syslog(handle, LOG_WARNING, "Failed to parse suspend-please= argument, ignoring: %s", v); + else if (please_suspend) + *please_suspend = k; + + } else if ((v = startswith(argv[i], "debug="))) { + int k; + + k = parse_boolean(v); + if (k < 0) + pam_syslog(handle, LOG_WARNING, "Failed to parse debug= argument, ignoring: %s", v); + else if (debug) + *debug = k; + + } else + pam_syslog(handle, LOG_WARNING, "Unknown parameter '%s', ignoring", argv[i]); + } + + return 0; +} + +static int acquire_user_record( + pam_handle_t *handle, + UserRecord **ret_record) { + + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; + const char *username = NULL, *json = NULL; + const void *b = NULL; + int r; + + assert(handle); + + r = pam_get_user(handle, &username, NULL); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to get user name: %s", pam_strerror(handle, r)); + return r; + } + + if (isempty(username)) { + pam_syslog(handle, LOG_ERR, "User name not valid."); + return PAM_SERVICE_ERR; + } + + /* Let's bypass all IPC complexity for the two user names we know for sure we don't manage. */ + if (STR_IN_SET(username, "root", NOBODY_USER_NAME)) + return PAM_USER_UNKNOWN; + + /* Let's check if a previous run determined that this user is not managed by homed. If so, let's exit early */ + r = pam_get_data(handle, "systemd-user-record-is-homed", &b); + if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) { + /* Failure */ + pam_syslog(handle, LOG_ERR, "Failed to get PAM user record is homed flag: %s", pam_strerror(handle, r)); + return r; + } else if (b == NULL) + /* Nothing cached yet, need to acquire fresh */ + json = NULL; + else if (b != USER_RECORD_IS_HOMED) + /* Definitely not a homed record */ + return PAM_USER_UNKNOWN; + else { + /* It's a homed record, let's use the cache, so that we can share it between the session and + * the authentication hooks */ + r = pam_get_data(handle, "systemd-user-record", (const void**) &json); + 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; + } + } + + if (!json) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_free_ char *json_copy = NULL; + + r = pam_acquire_bus_connection(handle, &bus); + if (r != PAM_SUCCESS) + return r; + + r = sd_bus_call_method( + bus, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "GetUserRecordByName", + &error, + &reply, + "s", + username); + if (r < 0) { + if (sd_bus_error_has_name(&error, SD_BUS_ERROR_SERVICE_UNKNOWN) || + sd_bus_error_has_name(&error, SD_BUS_ERROR_NAME_HAS_NO_OWNER)) { + pam_syslog(handle, LOG_DEBUG, "systemd-homed is not available: %s", bus_error_message(&error, r)); + goto user_unknown; + } + + if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_HOME)) { + pam_syslog(handle, LOG_DEBUG, "Not a user managed by systemd-homed: %s", bus_error_message(&error, r)); + goto user_unknown; + } + + pam_syslog(handle, LOG_ERR, "Failed to query user record: %s", bus_error_message(&error, r)); + return PAM_SERVICE_ERR; + } + + r = sd_bus_message_read(reply, "sbo", &json, NULL, NULL); + if (r < 0) + return pam_bus_log_parse_error(handle, r); + + json_copy = strdup(json); + if (!json_copy) + return pam_log_oom(handle); + + r = pam_set_data(handle, "systemd-user-record", json_copy, pam_cleanup_free); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM user record data: %s", pam_strerror(handle, r)); + return r; + } + + TAKE_PTR(json_copy); + + r = pam_set_data(handle, "systemd-user-record-is-homed", USER_RECORD_IS_HOMED, NULL); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM user record is homed flag: %s", pam_strerror(handle, r)); + return r; + } + } + + 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; + } + + if (!streq_ptr(username, ur->user_name)) { + pam_syslog(handle, LOG_ERR, "Acquired user record does not match user name."); + return PAM_SERVICE_ERR; + } + + if (ret_record) + *ret_record = TAKE_PTR(ur); + + return PAM_SUCCESS; + +user_unknown: + /* Cache this, so that we don't check again */ + r = pam_set_data(handle, "systemd-user-record-is-homed", USER_RECORD_IS_OTHER, NULL); + if (r != PAM_SUCCESS) + pam_syslog(handle, LOG_ERR, "Failed to set PAM user record is homed flag, ignoring: %s", pam_strerror(handle, r)); + + return PAM_USER_UNKNOWN; +} + +static int release_user_record(pam_handle_t *handle) { + int r, k; + + r = pam_set_data(handle, "systemd-user-record", NULL, NULL); + if (r != PAM_SUCCESS) + pam_syslog(handle, LOG_ERR, "Failed to release PAM user record data: %s", pam_strerror(handle, r)); + + k = pam_set_data(handle, "systemd-user-record-is-homed", NULL, NULL); + if (k != PAM_SUCCESS) + pam_syslog(handle, LOG_ERR, "Failed to release PAM user record is homed flag: %s", pam_strerror(handle, k)); + + return IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA) ? k : r; +} + +static void cleanup_home_fd(pam_handle_t *handle, void *data, int error_status) { + safe_close(PTR_TO_FD(data)); +} + +static int acquire_home( + pam_handle_t *handle, + bool please_authenticate, + bool please_suspend, + bool debug) { + + _cleanup_(user_record_unrefp) UserRecord *ur = NULL, *secret = NULL; + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; + _cleanup_close_ int acquired_fd = -1; + const void *home_fd_ptr = NULL; + const char *password = NULL; + unsigned n_attempts = 0; + int r; + + assert(handle); + + /* This acquires a reference to a home directory in one of two ways: if please_authenticate is true, + * then we'll call AcquireHome() after asking the user for a password. Otherwise it tries to call + * RefHome() and if that fails queries the user for a password and uses AcquireHome(). + * + * The idea is that the PAM authentication hook sets please_authenticate and thus always + * authenticates, while the other PAM hooks unset it so that they can a ref of their own without + * authentication if possible, but with authentication if necessary. */ + + /* If we already have acquired the fd, let's shortcut this */ + r = pam_get_data(handle, "systemd-home-fd", &home_fd_ptr); + if (r == PAM_SUCCESS && PTR_TO_INT(home_fd_ptr) >= 0) + return PAM_SUCCESS; + + r = pam_acquire_bus_connection(handle, &bus); + if (r != PAM_SUCCESS) + return r; + + r = acquire_user_record(handle, &ur); + if (r != PAM_SUCCESS) + return r; + + /* Eventually, we should analyze the record we got here, and if there's some other authentication + * data configured in it query for that. For now we don't care and just unconditionally ask for a + * password. */ + + if (please_authenticate) { + r = pam_get_authtok(handle, PAM_AUTHTOK, &password, "Password: "); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to get password: %s", pam_strerror(handle, r)); + return r; + } + if (isempty(password)) { + pam_syslog(handle, LOG_DEBUG, "Password request aborted."); + return PAM_AUTHTOK_ERR; + } + + secret = user_record_new(); + if (!secret) + return pam_log_oom(handle); + + r = user_record_set_password(secret, STRV_MAKE(password), true); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to store password: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + } + + /* Implement our own retry loop here instead of relying on the PAM client's one. That's because it + * might happen that the the record we stored on the host does not match the encryption password of + * the LUKS image in case the image was used in a different system where the password was + * changed. In that case it will happen that the LUKS password and the host password are + * different, and we handle that by collecting and passing multiple passwords in that case. Hence we + * treat bad passwords as a request to collect one more password and pass the new all all previously + * used passwords again. */ + + for (;;) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(erase_and_freep) char *newp = NULL; + + password = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + secret ? "AcquireHome" : "RefHome"); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = sd_bus_message_append(m, "s", ur->user_name); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + if (secret) { + r = bus_message_append_secret(m, secret); + if (r < 0) + return pam_bus_log_create_error(handle, r); + } + + r = sd_bus_message_append(m, "b", please_suspend); + if (r < 0) + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_ABSENT)) { + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Home of user %s is currently absent, please plug in the necessary storage device or backing file system.", ur->user_name); + pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r)); + return PAM_PERM_DENIED; + } else if (sd_bus_error_has_name(&error, BUS_ERROR_AUTHENTICATION_LIMIT_HIT)) { + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Too frequent unsuccessful login attempts for user %s, try again later.", ur->user_name); + pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r)); + return PAM_MAXTRIES; + } else if (sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) { + + if (++n_attempts >= 5) { + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Too many unsuccessful login attempts for user %s, refusing.", ur->user_name); + pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r)); + return PAM_MAXTRIES; + } + + /* This didn't work? Ask for an additional password */ + + r = pam_prompt(handle, PAM_PROMPT_ECHO_OFF, &newp, "Password incorrect or not sufficient for authentication of user %s, please try again: ", ur->user_name); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to prompt for additional password: %s", pam_strerror(handle, r)); + return r; + } + + password = newp; + + } else if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_NOT_ACTIVE) || + sd_bus_error_has_name(&error, BUS_ERROR_HOME_LOCKED)) { + + int q; + + /* Only on RefHome(): We can't access the home directory currently, unless + * it's unlocked with a password. Try to do so hence, ask for a password. */ + + q = pam_get_authtok(handle, PAM_AUTHTOK, &password, "Password: "); + if (q != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r)); + pam_syslog(handle, LOG_DEBUG, "Failed to get password: %s", pam_strerror(handle, q)); + + /* This will fail in certain environments, for example when we are + * called from OpenSSH's account or session hooks, or in systemd's + * per-service PAM logic. In that case, print a friendly message and + * accept failure. */ + + if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_NOT_ACTIVE)) + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Home of user %s is currently not active, please log in locally first.", ur->user_name); + else { + assert(sd_bus_error_has_name(&error, BUS_ERROR_HOME_LOCKED)); + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Home of user %s is currently locked, please unlock locally first.", ur->user_name); + } + + return PAM_PERM_DENIED; + } + } else { + pam_syslog(handle, LOG_ERR, "Failed to acquire home for user %s: %s", ur->user_name, bus_error_message(&error, r)); + return PAM_SERVICE_ERR; + } + + if (isempty(password)) { + pam_syslog(handle, LOG_DEBUG, "Password request aborted."); + return PAM_AUTHTOK_ERR; + } + } else { + int fd; + + r = sd_bus_message_read(reply, "h", &fd); + if (r < 0) + return pam_bus_log_parse_error(handle, r); + + acquired_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3); + if (acquired_fd < 0) { + pam_syslog(handle, LOG_ERR, "Failed to duplicate acquired fd: %s", bus_error_message(&error, r)); + return PAM_SERVICE_ERR; + } + + break; + } + + if (password) { + /* We acquire a new password? Then add it to our set of secrets */ + if (!secret) { + secret = user_record_new(); + if (!secret) + return pam_log_oom(handle); + } + + /* Add the password to the list of passwords we already have, in case the + * record and LUKS passwords differ and we need multiple to unlock things */ + r = user_record_set_password(secret, STRV_MAKE(password), true); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to store password: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + } + + /* Try again */ + } + + r = pam_set_data(handle, "systemd-home-fd", FD_TO_PTR(acquired_fd), cleanup_home_fd); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM bus data: %s", pam_strerror(handle, r)); + return r; + } + TAKE_FD(acquired_fd); + + if (secret) { + /* We likely just activated the home directory, let's flush out the user record, since a + * newer embedded user record might have been acquired from the activation. */ + + r = release_user_record(handle); + if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) + return r; + } + + pam_syslog(handle, LOG_NOTICE, "Home for user %s successfully acquired.", ur->user_name); + + return PAM_SUCCESS; +} + +static int release_home_fd(pam_handle_t *handle) { + const void *home_fd_ptr = NULL; + int r; + + r = pam_get_data(handle, "systemd-home-fd", &home_fd_ptr); + if (r == PAM_NO_MODULE_DATA || PTR_TO_FD(home_fd_ptr) < 0) + return PAM_NO_MODULE_DATA; + + r = pam_set_data(handle, "systemd-home-fd", NULL, NULL); + if (r != PAM_SUCCESS) + pam_syslog(handle, LOG_ERR, "Failed to release PAM home reference fd: %s", pam_strerror(handle, r)); + + return r; +} + +_public_ PAM_EXTERN int pam_sm_authenticate( + pam_handle_t *handle, + int flags, + int argc, const char **argv) { + + bool debug = true, suspend_please = false; + + if (parse_argv(handle, + argc, argv, + &suspend_please, + &debug) < 0) + return PAM_AUTH_ERR; + + if (debug) + pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed authenticating"); + + return acquire_home(handle, /* please_authenticate= */ true, suspend_please, debug); +} + +_public_ PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) { + return PAM_SUCCESS; +} + +_public_ PAM_EXTERN int pam_sm_open_session( + pam_handle_t *handle, + int flags, + int argc, const char **argv) { + + bool debug = true, suspend_please = false; + int r; + + if (parse_argv(handle, + argc, argv, + &suspend_please, + &debug) < 0) + return PAM_SESSION_ERR; + + if (debug) + pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed session start"); + + r = acquire_home(handle, /* please_authenticate = */ false, suspend_please, debug); + if (r == PAM_USER_UNKNOWN) /* Not managed by us? Don't complain. */ + return PAM_SUCCESS; + if (r != PAM_SUCCESS) + return r; + + r = pam_putenv(handle, "SYSTEMD_HOME=1"); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable $SYSTEMD_HOME: %s", pam_strerror(handle, r)); + 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 process 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; +} + +_public_ PAM_EXTERN int pam_sm_close_session( + pam_handle_t *handle, + int flags, + int argc, const char **argv) { + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; + const char *username = NULL; + bool debug = true; + int r; + + if (parse_argv(handle, + argc, argv, + NULL, + &debug) < 0) + return PAM_SESSION_ERR; + + if (debug) + pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed session end"); + + /* Let's explicitly drop the reference to the homed session, so that the subsequent ReleaseHome() + * call will be able to do its thing. */ + r = release_home_fd(handle); + if (r == PAM_NO_MODULE_DATA) /* Nothing to do, we never acquired an fd */ + return PAM_SUCCESS; + if (r != PAM_SUCCESS) + return r; + + r = pam_get_user(handle, &username, NULL); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to get user name: %s", pam_strerror(handle, r)); + return r; + } + + r = pam_acquire_bus_connection(handle, &bus); + if (r != PAM_SUCCESS) + return r; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ReleaseHome"); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = sd_bus_message_append(m, "s", username); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_BUSY)) + pam_syslog(handle, LOG_NOTICE, "Not deactivating home directory of %s, as it is still used.", username); + else { + pam_syslog(handle, LOG_ERR, "Failed to release user home: %s", bus_error_message(&error, r)); + return PAM_SESSION_ERR; + } + } + + return PAM_SUCCESS; +} + +_public_ PAM_EXTERN int pam_sm_acct_mgmt( + pam_handle_t *handle, + int flags, + int argc, + const char **argv) { + + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + bool debug = true, please_suspend = false; + usec_t t; + int r; + + if (parse_argv(handle, + argc, argv, + &please_suspend, + &debug) < 0) + return PAM_AUTH_ERR; + + if (debug) + pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed account management"); + + r = acquire_home(handle, /* please_authenticate = */ false, please_suspend, debug); + if (r == PAM_USER_UNKNOWN) + return PAM_SUCCESS; /* we don't have anything to say about users we don't manage */ + if (r != PAM_SUCCESS) + return r; + + r = acquire_user_record(handle, &ur); + if (r != PAM_SUCCESS) + return r; + + r = user_record_test_blocked(ur); + switch (r) { + + case -ESTALE: + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is newer than current system time, prohibiting access."); + return PAM_ACCT_EXPIRED; + + case -ENOLCK: + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is blocked, prohibiting access."); + return PAM_ACCT_EXPIRED; + + case -EL2HLT: + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is not valid yet, prohibiting access."); + return PAM_ACCT_EXPIRED; + + case -EL3HLT: + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record is not valid anymore, prohibiting access."); + return PAM_ACCT_EXPIRED; + + default: + if (r < 0) { + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "User record not valid, prohibiting access."); + return PAM_ACCT_EXPIRED; + } + + break; + } + + t = user_record_ratelimit_next_try(ur); + if (t != USEC_INFINITY) { + usec_t n = now(CLOCK_REALTIME); + + if (t > n) { + char buf[FORMAT_TIMESPAN_MAX]; + (void) pam_prompt(handle, PAM_ERROR_MSG, NULL, "Too many logins, try again in %s.", + format_timespan(buf, sizeof(buf), t - n, USEC_PER_SEC)); + + return PAM_MAXTRIES; + } + } + + return PAM_SUCCESS; +} + +_public_ PAM_EXTERN int pam_sm_chauthtok( + pam_handle_t *handle, + int flags, + int argc, + const char **argv) { + + _cleanup_(user_record_unrefp) UserRecord *ur = NULL, *old_secret = NULL, *new_secret = NULL; + const char *old_password = NULL, *new_password = NULL; + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; + unsigned n_attempts = 0; + bool debug = true; + int r; + + if (parse_argv(handle, + argc, argv, + NULL, + &debug) < 0) + return PAM_AUTH_ERR; + + if (debug) + pam_syslog(handle, LOG_DEBUG, "pam-systemd-homed account management"); + + r = pam_acquire_bus_connection(handle, &bus); + if (r != PAM_SUCCESS) + return r; + + r = acquire_user_record(handle, &ur); + if (r != PAM_SUCCESS) + return r; + + /* Regardless if in the PAM_PRELIM_CHECK phase or not let's ask for the passwords with the right + * prompts, so that the password is definitely cached. */ + r = pam_get_authtok(handle, PAM_OLDAUTHTOK, &old_password, "Old password: "); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to get old password: %s", pam_strerror(handle, r)); + return r; + } + if (isempty(old_password)) { + pam_syslog(handle, LOG_DEBUG, "Password request aborted."); + return PAM_AUTHTOK_ERR; + } + + /* Is the new password already cached? */ + r = pam_get_item(handle, PAM_AUTHTOK, (const void**) &new_password); + if (!IN_SET(r, PAM_BAD_ITEM, PAM_SUCCESS)) { + pam_syslog(handle, LOG_ERR, "Failed to get cached password: %s", pam_strerror(handle, r)); + return r; + } + + if (isempty(new_password)) { + /* No, it's not cached, then let's ask for the password and its verification, and cache + * it. */ + + r = pam_get_authtok_noverify(handle, &new_password, "New password: "); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to get new password: %s", pam_strerror(handle, r)); + return r; + } + if (isempty(new_password)) { + pam_syslog(handle, LOG_DEBUG, "Password request aborted."); + return PAM_AUTHTOK_ERR; + } + + r = pam_get_authtok_verify(handle, &new_password, "new password: "); /* Lower case, since PAM prefixes 'Repeat' */ + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to get password again: %s", pam_strerror(handle, r)); + return r; + } + + // FIXME: pam_pwquality will ask for the password a third time. It really shouldn't do + // that, and instead assume the password was already verified once when it is found to be + // cached already. needs to be fixed in pam_pwquality + } + + /* Now everything is cached and checked, let's exit from the preliminary check */ + if (FLAGS_SET(flags, PAM_PRELIM_CHECK)) + return PAM_SUCCESS; + + old_secret = user_record_new(); + if (!old_secret) + return pam_log_oom(handle); + + r = user_record_set_password(old_secret, STRV_MAKE(old_password), true); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to store old password: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + new_secret = user_record_new(); + if (!new_secret) + return pam_log_oom(handle); + + r = user_record_set_password(new_secret, STRV_MAKE(new_password), true); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to store new password: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + for (;;) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "ChangePasswordHome"); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = sd_bus_message_append(m, "s", ur->user_name); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = bus_message_append_secret(m, new_secret); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = bus_message_append_secret(m, old_secret); + if (r < 0) + return pam_bus_log_create_error(handle, r); + + r = sd_bus_call(bus, m, HOME_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_BUSY)) { + pam_error(handle, "Home of user %s is currently being modified, please come back later.", ur->user_name); + return PAM_PERM_DENIED; + } else if (sd_bus_error_has_name(&error, BUS_ERROR_HOME_ABSENT)) { + pam_error(handle, "Home of user %s is currently absent, please plug in the necessary storage device or backing file system.", ur->user_name); + return PAM_PERM_DENIED; + } else if (!sd_bus_error_has_name(&error, BUS_ERROR_BAD_PASSWORD)) { + pam_syslog(handle, LOG_ERR, "Failed to change password for home: %s", bus_error_message(&error, r)); + return PAM_SERVICE_ERR; + } + } else { + pam_syslog(handle, LOG_NOTICE, "Successfully changed password for user %s.", ur->user_name); + return PAM_SUCCESS; + } + + /* Invalidate old token */ + r = pam_set_item(handle, PAM_OLDAUTHTOK, NULL); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to reset cached password: %s", pam_strerror(handle, r)); + return r; + } + + if (++n_attempts >= 5) + break; + + /* Acquire new old token */ + r = pam_get_authtok(handle, PAM_OLDAUTHTOK, &old_password, + "Old password incorrect or not sufficient for authentication, please try again: "); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to get old password: %s", pam_strerror(handle, r)); + return r; + } + if (isempty(old_password)) { + pam_syslog(handle, LOG_DEBUG, "Password request aborted."); + return PAM_AUTHTOK_ERR; + } + + r = user_record_set_password(old_secret, STRV_MAKE(old_password), true); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to set old password: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + /* Try again */ + }; + + pam_syslog(handle, LOG_NOTICE, "Failed to change password for user %s: %m", ur->user_name); + return PAM_MAXTRIES; +} diff --git a/src/home/pam_systemd_home.sym b/src/home/pam_systemd_home.sym new file mode 100644 index 0000000000000..daec0499e1148 --- /dev/null +++ b/src/home/pam_systemd_home.sym @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +{ +global: + pam_sm_authenticate; + pam_sm_setcred; + pam_sm_open_session; + pam_sm_close_session; + pam_sm_acct_mgmt; + pam_sm_chauthtok; +local: *; +}; diff --git a/src/home/pwquality-util.c b/src/home/pwquality-util.c new file mode 100644 index 0000000000000..be4fb1cb4a071 --- /dev/null +++ b/src/home/pwquality-util.c @@ -0,0 +1,182 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#if HAVE_LIBPWQUALITY +/* pwquality.h uses size_t but doesn't include sys/types.h on its own */ +#include +#include +#endif + +#include "bus-common-errors.h" +#include "home-util.h" +#include "memory-util.h" +#include "pwquality-util.h" +#include "strv.h" + +#if HAVE_LIBPWQUALITY +DEFINE_TRIVIAL_CLEANUP_FUNC(pwquality_settings_t*, pwquality_free_settings); + +static void pwquality_maybe_disable_dictionary( + pwquality_settings_t *pwq) { + + char buf[PWQ_MAX_ERROR_MESSAGE_LEN]; + const char *path; + int r; + + r = pwquality_get_str_value(pwq, PWQ_SETTING_DICT_PATH, &path); + if (r < 0) { + log_warning("Failed to read libpwquality dictionary path, ignoring: %s", pwquality_strerror(buf, sizeof(buf), r, NULL)); + return; + } + + // REMOVE THIS AS SOON AS https://github.com/libpwquality/libpwquality/pull/21 IS MERGED AND RELEASED + if (isempty(path)) + path = "/usr/share/cracklib/pw_dict.pwd.gz"; + + if (isempty(path)) { + log_warning("Weird, no dictionary file configured, ignoring."); + return; + } + + if (access(path, F_OK) >= 0) + return; + + if (errno != ENOENT) { + log_warning_errno(errno, "Failed to check if dictionary file %s exists, ignoring: %m", path); + return; + } + + r = pwquality_set_int_value(pwq, PWQ_SETTING_DICT_CHECK, 0); + if (r < 0) { + log_warning("Failed to disable libpwquality dictionary check, ignoring: %s", pwquality_strerror(buf, sizeof(buf), r, NULL)); + return; + } +} + +int quality_check_password( + UserRecord *hr, + UserRecord *secret, + sd_bus_error *error) { + + _cleanup_(pwquality_free_settingsp) pwquality_settings_t *pwq = NULL; + char buf[PWQ_MAX_ERROR_MESSAGE_LEN], **pp; + void *auxerror; + int r; + + assert(hr); + assert(secret); + + pwq = pwquality_default_settings(); + if (!pwq) + return log_oom(); + + r = pwquality_read_config(pwq, NULL, &auxerror); + if (r < 0) + log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to read libpwquality configuation, ignoring: %s", + pwquality_strerror(buf, sizeof(buf), r, auxerror)); + + pwquality_maybe_disable_dictionary(pwq); + + /* This is a bit more complex than one might think at first. pwquality_check() would like to know the + * old password to make security checks. We support arbitrary numbers of passwords however, hence we + * call the function once for each combination of old and new password. */ + + /* Iterate through all new passwords */ + STRV_FOREACH(pp, secret->password) { + bool called = false; + char **old; + + r = test_password(hr->hashed_password, *pp); + if (r < 0) + return r; + if (r == 0) /* This is an old password as it isn't listed in the hashedPassword field, skip it */ + continue; + + /* Check this password against all old passwords */ + STRV_FOREACH(old, secret->password) { + + if (streq(*pp, *old)) + continue; + + r = test_password(hr->hashed_password, *old); + if (r < 0) + return r; + if (r > 0) /* This is a new password, not suitable as old password */ + continue; + + r = pwquality_check(pwq, *pp, *old, hr->user_name, &auxerror); + if (r < 0) + return sd_bus_error_setf(error, BUS_ERROR_LOW_PASSWORD_QUALITY, "Password too weak: %s", + pwquality_strerror(buf, sizeof(buf), r, auxerror)); + + called = true; + } + + if (called) + continue; + + /* If there are no old passwords, let's call pwquality_check() without any. */ + r = pwquality_check(pwq, *pp, NULL, hr->user_name, &auxerror); + if (r < 0) + return sd_bus_error_setf(error, BUS_ERROR_LOW_PASSWORD_QUALITY, "Password too weak: %s", + pwquality_strerror(buf, sizeof(buf), r, auxerror)); + } + + return 0; +} + +#define N_SUGGESTIONS 6 + +int suggest_passwords(void) { + _cleanup_(pwquality_free_settingsp) pwquality_settings_t *pwq = NULL; + _cleanup_strv_free_erase_ char **suggestions = NULL; + _cleanup_(erase_and_freep) char *joined = NULL; + char buf[PWQ_MAX_ERROR_MESSAGE_LEN]; + void *auxerror; + size_t i; + int r; + + pwq = pwquality_default_settings(); + if (!pwq) + return log_oom(); + + r = pwquality_read_config(pwq, NULL, &auxerror); + if (r < 0) + log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to read libpwquality configuation, ignoring: %s", + pwquality_strerror(buf, sizeof(buf), r, auxerror)); + + pwquality_maybe_disable_dictionary(pwq); + + suggestions = new0(char*, N_SUGGESTIONS); + if (!suggestions) + return log_oom(); + + for (i = 0; i < N_SUGGESTIONS; i++) { + _cleanup_free_ char *suggestion = NULL; + + r = pwquality_generate(pwq, 64, suggestions + i); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to generate password, ignoring: %s", + pwquality_strerror(buf, sizeof(buf), r, NULL)); + } + + joined = strv_join(suggestions, " "); + if (!joined) + return log_oom(); + + log_info("Password suggestions: %s", joined); + return 0; +} + +#else + +int quality_check_password(UserRecord *hr, sd_bus_error *error) { + assert(hr); + return 0; +} + +int suggest_passwords(void) { + return 0; +} +#endif diff --git a/src/home/pwquality-util.h b/src/home/pwquality-util.h new file mode 100644 index 0000000000000..d61c04c34216f --- /dev/null +++ b/src/home/pwquality-util.h @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "sd-bus.h" +#include "user-record.h" + +int quality_check_password(UserRecord *hr, UserRecord *secret, sd_bus_error *error); + +int suggest_passwords(void); diff --git a/src/home/user-record-sign.c b/src/home/user-record-sign.c new file mode 100644 index 0000000000000..d02c4750b37c4 --- /dev/null +++ b/src/home/user-record-sign.c @@ -0,0 +1,174 @@ +#include + +#include "fd-util.h" +#include "user-record-sign.h" +#include "fileio.h" + +static int user_record_signable_json(UserRecord *ur, char **ret) { + _cleanup_(user_record_unrefp) UserRecord *reduced = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *j = NULL; + int r; + + assert(ur); + assert(ret); + + r = user_record_clone(ur, USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PRIVILEGED|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_STRIP_SECRET|USER_RECORD_STRIP_BINDING|USER_RECORD_STRIP_STATUS|USER_RECORD_STRIP_SIGNATURE, &reduced); + if (r < 0) + return r; + + j = json_variant_ref(reduced->json); + + r = json_variant_normalize(&j); + if (r < 0) + return r; + + return json_variant_format(j, 0, ret); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(EVP_MD_CTX*, EVP_MD_CTX_free); + +int user_record_sign(UserRecord *ur, EVP_PKEY *private_key, UserRecord **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *encoded = NULL, *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *signed_ur = NULL; + _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *md_ctx = NULL; + _cleanup_free_ char *text = NULL, *key = NULL; + size_t signature_size = 0, key_size = 0; + _cleanup_free_ void *signature = NULL; + _cleanup_fclose_ FILE *mf = NULL; + int r; + + assert(ur); + assert(private_key); + assert(ret); + + r = user_record_signable_json(ur, &text); + if (r < 0) + return r; + + md_ctx = EVP_MD_CTX_new(); + if (!md_ctx) + return -ENOMEM; + + if (EVP_DigestSignInit(md_ctx, NULL, NULL, NULL, private_key) <= 0) + return -EIO; + + /* Request signature size */ + if (EVP_DigestSign(md_ctx, NULL, &signature_size, (uint8_t*) text, strlen(text)) <= 0) + return -EIO; + + signature = malloc(signature_size); + if (!signature) + return -ENOMEM; + + if (EVP_DigestSign(md_ctx, signature, &signature_size, (uint8_t*) text, strlen(text)) <= 0) + return -EIO; + + mf = open_memstream_unlocked(&key, &key_size); + if (!mf) + return -ENOMEM; + + if (PEM_write_PUBKEY(mf, private_key) <= 0) + return -EIO; + + r = fflush_and_check(mf); + if (r < 0) + return r; + + r = json_build(&encoded, JSON_BUILD_ARRAY( + JSON_BUILD_OBJECT(JSON_BUILD_PAIR("data", JSON_BUILD_BASE64(signature, signature_size)), + JSON_BUILD_PAIR("key", JSON_BUILD_STRING(key))))); + if (r < 0) + return r; + + v = json_variant_ref(ur->json); + + r = json_variant_set_field(&v, "signature", encoded); + if (r < 0) + return r; + + if (DEBUG_LOGGING) + json_variant_dump(v, JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO, NULL, NULL); + + signed_ur = user_record_new(); + if (!signed_ur) + return log_oom(); + + r = user_record_load(signed_ur, v, USER_RECORD_LOAD_FULL); + if (r < 0) + return r; + + *ret = TAKE_PTR(signed_ur); + return 0; +} + +int user_record_verify(UserRecord *ur, EVP_PKEY *public_key) { + _cleanup_free_ char *text = NULL, *key = NULL; + unsigned n_good = 0, n_bad = 0; + JsonVariant *array, *e; + int r; + + assert(ur); + assert(public_key); + + array = json_variant_by_key(ur->json, "signature"); + if (!array) + return USER_RECORD_UNSIGNED; + + if (!json_variant_is_array(array)) + return -EINVAL; + + if (json_variant_elements(array) == 0) + return USER_RECORD_UNSIGNED; + + r = user_record_signable_json(ur, &text); + if (r < 0) + return r; + + JSON_VARIANT_ARRAY_FOREACH(e, array) { + _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *md_ctx = NULL; + _cleanup_free_ void *signature = NULL; + size_t signature_size = 0; + JsonVariant *data; + + if (!json_variant_is_object(e)) + return -EINVAL; + + data = json_variant_by_key(e, "data"); + if (!data) + return -EINVAL; + + r = json_variant_unbase64(data, &signature, &signature_size); + if (r < 0) + return r; + + md_ctx = EVP_MD_CTX_new(); + if (!md_ctx) + return -ENOMEM; + + if (EVP_DigestVerifyInit(md_ctx, NULL, NULL, NULL, public_key) <= 0) + return -EIO; + + if (EVP_DigestVerify(md_ctx, signature, signature_size, (uint8_t*) text, strlen(text)) <= 0) { + n_bad ++; + continue; + } + + n_good ++; + } + + return n_good > 0 ? (n_bad == 0 ? USER_RECORD_SIGNED_EXCLUSIVE : USER_RECORD_SIGNED) : + (n_bad == 0 ? USER_RECORD_UNSIGNED : USER_RECORD_FOREIGN); +} + +int user_record_has_signature(UserRecord *ur) { + JsonVariant *array; + + array = json_variant_by_key(ur->json, "signature"); + if (!array) + return false; + + if (!json_variant_is_array(array)) + return -EINVAL; + + return json_variant_elements(array) > 0; +} diff --git a/src/home/user-record-sign.h b/src/home/user-record-sign.h new file mode 100644 index 0000000000000..f045c8837bced --- /dev/null +++ b/src/home/user-record-sign.h @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +#include "user-record.h" + +int user_record_sign(UserRecord *ur, EVP_PKEY *private_key, UserRecord **ret); + +enum { + USER_RECORD_UNSIGNED, /* user record has no signature */ + USER_RECORD_SIGNED_EXCLUSIVE, /* user record has only a signature by our own key */ + USER_RECORD_SIGNED, /* user record is signed by us, but by others too */ + USER_RECORD_FOREIGN, /* user record is not signed by us, but by others */ +}; + +int user_record_verify(UserRecord *ur, EVP_PKEY *public_key); + +int user_record_has_signature(UserRecord *ur); diff --git a/src/home/user-record-util.c b/src/home/user-record-util.c new file mode 100644 index 0000000000000..10adee848f6b9 --- /dev/null +++ b/src/home/user-record-util.c @@ -0,0 +1,1007 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#if HAVE_CRYPT_H +#include +#endif + +#include "errno-util.h" +#include "home-util.h" +#include "id128-util.h" +#include "mountpoint-util.h" +#include "path-util.h" +#include "stat-util.h" +#include "user-record-util.h" +#include "user-util.h" + +int user_record_synthesize( + UserRecord *h, + const char *user_name, + const char *realm, + const char *image_path, + UserStorage storage, + uid_t uid, + gid_t gid) { + + _cleanup_free_ char *hd = NULL, *un = NULL, *ip = NULL, *rr = NULL, *user_name_and_realm = NULL; + char smid[SD_ID128_STRING_MAX]; + sd_id128_t mid; + int r; + + assert(h); + assert(user_name); + assert(image_path); + assert(IN_SET(storage, USER_LUKS, USER_SUBVOLUME, USER_FSCRYPT, USER_DIRECTORY)); + assert(uid_is_valid(uid)); + assert(gid_is_valid(gid)); + + /* Fill in a home record from just a username and an image path. */ + + if (h->json) + return -EBUSY; + + if (!suitable_user_name(user_name)) + return -EINVAL; + + if (realm) { + r = suitable_realm(realm); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + } + + if (!suitable_image_path(image_path)) + return -EINVAL; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + un = strdup(user_name); + if (!un) + return -ENOMEM; + + if (realm) { + rr = strdup(realm); + if (!rr) + return -ENOMEM; + + user_name_and_realm = strjoin(user_name, "@", realm); + if (!user_name_and_realm) + return -ENOMEM; + } + + ip = strdup(image_path); + if (!ip) + return -ENOMEM; + + hd = path_join("/home/", user_name); + if (!hd) + return -ENOMEM; + + r = json_build(&h->json, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(user_name)), + JSON_BUILD_PAIR_CONDITION(!!rr, "realm", JSON_BUILD_STRING(realm)), + JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("regular")), + JSON_BUILD_PAIR("binding", JSON_BUILD_OBJECT( + JSON_BUILD_PAIR(sd_id128_to_string(mid, smid), JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("imagePath", JSON_BUILD_STRING(image_path)), + JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_STRING(hd)), + JSON_BUILD_PAIR("storage", JSON_BUILD_STRING(user_storage_to_string(storage))), + JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(uid)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(gid)))))))); + if (r < 0) + return r; + + free_and_replace(h->user_name, un); + free_and_replace(h->realm, rr); + free_and_replace(h->user_name_and_realm_auto, user_name_and_realm); + free_and_replace(h->image_path, ip); + free_and_replace(h->home_directory, hd); + h->storage = storage; + h->uid = uid; + + h->mask = USER_RECORD_REGULAR|USER_RECORD_BINDING; + return 0; +} + +int group_record_synthesize(GroupRecord *g, UserRecord *h) { + _cleanup_free_ char *un = NULL, *rr = NULL, *group_name_and_realm = NULL; + char smid[SD_ID128_STRING_MAX]; + sd_id128_t mid; + int r; + + assert(g); + assert(h); + + if (g->json) + return -EBUSY; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + un = strdup(h->user_name); + if (!un) + return -ENOMEM; + + if (h->realm) { + rr = strdup(h->realm); + if (!rr) + return -ENOMEM; + + group_name_and_realm = strjoin(un, "@", rr); + if (!group_name_and_realm) + return -ENOMEM; + } + + r = json_build(&g->json, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(un)), + JSON_BUILD_PAIR_CONDITION(!!rr, "realm", JSON_BUILD_STRING(rr)), + JSON_BUILD_PAIR("binding", JSON_BUILD_OBJECT( + JSON_BUILD_PAIR(sd_id128_to_string(mid, smid), JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(user_record_gid(h))))))), + JSON_BUILD_PAIR_CONDITION(h->disposition >= 0, "disposition", JSON_BUILD_STRING(user_disposition_to_string(user_record_disposition(h)))), + JSON_BUILD_PAIR("status", JSON_BUILD_OBJECT( + JSON_BUILD_PAIR(sd_id128_to_string(mid, smid), JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.Home")))))))); + if (r < 0) + return r; + + free_and_replace(g->group_name, un); + free_and_replace(g->realm, rr); + free_and_replace(g->group_name_and_realm_auto, group_name_and_realm); + g->gid = user_record_gid(h); + g->disposition = h->disposition; + + g->mask = USER_RECORD_REGULAR|USER_RECORD_BINDING; + return 0; +} + +int user_record_reconcile( + UserRecord *host, + UserRecord *embedded, + UserReconcileMode mode, + UserRecord **ret) { + + int r, result; + + /* Reconciles the identity record stored on the host with the one embedded in a $HOME + * directory. Returns the following error codes: + * + * -EINVAL: one of the records not valid + * -REMCHG: identity records are not about the same user + * -ESTALE: embedded identity record is equally new or newer than supplied record + * + * Return the new record to use, which is either the the embedded record updated with the host + * binding or the host record. In both cases the secret data is stripped. */ + + assert(host); + assert(embedded); + + /* Make sure both records are initialized */ + if (!host->json || !embedded->json) + return -EINVAL; + + /* Ensure these records actually contain user data */ + if (!(embedded->mask & host->mask & USER_RECORD_REGULAR)) + return -EINVAL; + + /* Make sure the user name and realm matches */ + if (!user_record_compatible(host, embedded)) + return -EREMCHG; + + /* Embedded identities may not contain secrets or binding info*/ + if ((embedded->mask & (USER_RECORD_SECRET|USER_RECORD_BINDING)) != 0) + return -EINVAL; + + /* The embedded record checked out, let's now figure out which of the two identities we'll consider + * in effect from now on. We do this by checking the last change timestamp, and in doubt always let + * the embedded data win. */ + if (host->last_change_usec != UINT64_MAX && + (embedded->last_change_usec == UINT64_MAX || host->last_change_usec > embedded->last_change_usec)) + + /* The host version is definitely newer, either because it has a version at all and the + * embedded version doesn't or because it is numerically newer. */ + result = USER_RECONCILE_HOST_WON; + + else if (host->last_change_usec == embedded->last_change_usec) { + + /* The nominal version number of the host and the embedded identity is the same. If so, let's + * verify that, and tell the caller if we are ignoring embedded data. */ + + r = user_record_masked_equal(host, embedded, USER_RECORD_REGULAR|USER_RECORD_PRIVILEGED|USER_RECORD_PER_MACHINE); + if (r < 0) + return r; + if (r > 0) { + if (mode == USER_RECONCILE_REQUIRE_NEWER) + return -ESTALE; + + result = USER_RECONCILE_IDENTICAL; + } else + result = USER_RECONCILE_HOST_WON; + } else { + _cleanup_(json_variant_unrefp) JsonVariant *extended = NULL; + _cleanup_(user_record_unrefp) UserRecord *merged = NULL; + JsonVariant *e; + + /* The embedded version is newer */ + + if (mode == USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL) + return -ESTALE; + + /* Copy in the binding data */ + extended = json_variant_ref(embedded->json); + + e = json_variant_by_key(host->json, "binding"); + if (e) { + r = json_variant_set_field(&extended, "binding", e); + if (r < 0) + return r; + } + + merged = user_record_new(); + if (!merged) + return -ENOMEM; + + r = user_record_load(merged, extended, USER_RECORD_LOAD_MASK_SECRET); + if (r < 0) + return r; + + *ret = TAKE_PTR(merged); + return USER_RECONCILE_EMBEDDED_WON; /* update */ + } + + /* Strip out secrets */ + r = user_record_clone(host, USER_RECORD_LOAD_MASK_SECRET, ret); + if (r < 0) + return r; + + return result; +} + +int user_record_add_binding( + UserRecord *h, + UserStorage storage, + const char *image_path, + sd_id128_t partition_uuid, + sd_id128_t luks_uuid, + sd_id128_t fs_uuid, + const char *luks_cipher, + const char *luks_cipher_mode, + uint64_t luks_volume_key_size, + const char *file_system_type, + const void *fscrypt_salt, + size_t fscrypt_salt_size, + const char *home_directory, + uid_t uid, + gid_t gid) { + + _cleanup_(json_variant_unrefp) JsonVariant *new_binding_entry = NULL, *binding = NULL; + char smid[SD_ID128_STRING_MAX], partition_uuids[37], luks_uuids[37], fs_uuids[37]; + _cleanup_free_ char *ip = NULL, *hd = NULL; + _cleanup_free_ void *fs = NULL; + sd_id128_t mid; + int r; + + assert(h); + assert(fscrypt_salt_size == 0 || fscrypt_salt); + + if (!h->json) + return -EUNATCH; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + sd_id128_to_string(mid, smid); + + if (image_path) { + ip = strdup(image_path); + if (!ip) + return -ENOMEM; + } + + if (home_directory) { + hd = strdup(home_directory); + if (!hd) + return -ENOMEM; + } + + if (fscrypt_salt_size > 0) { + fs = memdup(fscrypt_salt, fscrypt_salt_size); + if (!fs) + return -ENOMEM; + } + + r = json_build(&new_binding_entry, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_CONDITION(!!image_path, "imagePath", JSON_BUILD_STRING(image_path)), + JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(partition_uuid), "partitionUUID", JSON_BUILD_STRING(id128_to_uuid_string(partition_uuid, partition_uuids))), + JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(luks_uuid), "luksUUID", JSON_BUILD_STRING(id128_to_uuid_string(luks_uuid, luks_uuids))), + JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(fs_uuid), "fileSystemUUID", JSON_BUILD_STRING(id128_to_uuid_string(fs_uuid, fs_uuids))), + JSON_BUILD_PAIR_CONDITION(!!luks_cipher, "luksCipher", JSON_BUILD_STRING(luks_cipher)), + JSON_BUILD_PAIR_CONDITION(!!luks_cipher_mode, "luksCipherMode", JSON_BUILD_STRING(luks_cipher_mode)), + JSON_BUILD_PAIR_CONDITION(luks_volume_key_size != UINT64_MAX, "luksVolumeKeySize", JSON_BUILD_UNSIGNED(luks_volume_key_size)), + JSON_BUILD_PAIR_CONDITION(!!file_system_type, "fileSystemType", JSON_BUILD_STRING(file_system_type)), + JSON_BUILD_PAIR_CONDITION(fscrypt_salt_size > 0, "fscryptSalt", JSON_BUILD_BASE64(fscrypt_salt, fscrypt_salt_size)), + JSON_BUILD_PAIR_CONDITION(!!home_directory, "homeDirectory", JSON_BUILD_STRING(home_directory)), + JSON_BUILD_PAIR_CONDITION(uid_is_valid(uid), "uid", JSON_BUILD_UNSIGNED(uid)), + JSON_BUILD_PAIR_CONDITION(gid_is_valid(gid), "gid", JSON_BUILD_UNSIGNED(gid)), + JSON_BUILD_PAIR_CONDITION(storage >= 0, "storage", JSON_BUILD_STRING(user_storage_to_string(storage))))); + if (r < 0) + return r; + + binding = json_variant_ref(json_variant_by_key(h->json, "binding")); + if (binding) { + _cleanup_(json_variant_unrefp) JsonVariant *be = NULL; + + /* Merge the new entry with an old one, if that exists */ + be = json_variant_ref(json_variant_by_key(binding, smid)); + if (be) { + r = json_variant_merge(&be, new_binding_entry); + if (r < 0) + return r; + + json_variant_unref(new_binding_entry); + new_binding_entry = TAKE_PTR(be); + } + } + + r = json_variant_set_field(&binding, smid, new_binding_entry); + if (r < 0) + return r; + + r = json_variant_set_field(&h->json, "binding", binding); + if (r < 0) + return r; + + if (storage >= 0) + h->storage = storage; + + if (ip) + free_and_replace(h->image_path, ip); + + if (!sd_id128_is_null(partition_uuid)) + h->partition_uuid = partition_uuid; + + if (!sd_id128_is_null(luks_uuid)) + h->luks_uuid = luks_uuid; + + if (!sd_id128_is_null(fs_uuid)) + h->file_system_uuid = fs_uuid; + + if (fscrypt_salt_size > 0) { + free_and_replace(h->fscrypt_salt, fs); + h->fscrypt_salt_size = fscrypt_salt_size; + } + + if (hd) + free_and_replace(h->home_directory, hd); + + if (uid_is_valid(uid)) + h->uid = uid; + + h->mask |= USER_RECORD_BINDING; + return 1; +} + +int user_record_test_home_directory(UserRecord *h) { + const char *hd; + int r; + + assert(h); + + /* Returns one of USER_TEST_ABSENT, USER_TEST_MOUNTED, USER_TEST_EXISTS on success */ + + hd = user_record_home_directory(h); + if (!hd) + return -ENXIO; + + r = is_dir(hd, false); + if (r == -ENOENT) + return USER_TEST_ABSENT; + if (r < 0) + return r; + if (r == 0) + return -ENOTDIR; + + r = path_is_mount_point(hd, NULL, 0); + if (r < 0) + return r; + if (r > 0) + return USER_TEST_MOUNTED; + + /* If the image path and the home directory are identical, then it's OK if the directory is + * populated. */ + if (IN_SET(user_record_storage(h), USER_CLASSIC, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT)) { + const char *ip; + + ip = user_record_image_path(h); + if (ip && path_equal(ip, hd)) + return USER_TEST_EXISTS; + } + + /* Otherwise it's not OK */ + r = dir_is_empty(hd); + if (r < 0) + return r; + if (r == 0) + return -EBUSY; + + return USER_TEST_EXISTS; +} + +int user_record_test_home_directory_and_warn(UserRecord *h) { + int r; + + assert(h); + + r = user_record_test_home_directory(h); + if (r == -ENXIO) + return log_error_errno(r, "User record lacks home directory, refusing."); + if (r == -ENOTDIR) + return log_error_errno(r, "Home directory %s is not a directory, refusing.", user_record_home_directory(h)); + if (r == -EBUSY) + return log_error_errno(r, "Home directory %s exists, is not mounted but populated, refusing.", user_record_home_directory(h)); + if (r < 0) + return log_error_errno(r, "Failed to test whether the home directory %s exists: %m", user_record_home_directory(h)); + + return r; +} + +int user_record_test_image_path(UserRecord *h) { + const char *ip; + struct stat st; + + assert(h); + + if (user_record_storage(h) == USER_CIFS) + return USER_TEST_UNDEFINED; + + ip = user_record_image_path(h); + if (!ip) + return -ENXIO; + + if (stat(ip, &st) < 0) { + if (errno == ENOENT) + return USER_TEST_ABSENT; + + return -errno; + } + + switch (user_record_storage(h)) { + + case USER_LUKS: + if (S_ISREG(st.st_mode)) + return USER_TEST_EXISTS; + if (S_ISBLK(st.st_mode)) { + /* For block devices we can't really be sure if the device referenced actually is the + * fs we look for or some other file system (think: what does /dev/sdb1 refer + * to?). Hence, let's return USER_TEST_MAYBE as an ambigious return value for these + * case, except if the device path used is one of the paths that is based on a + * filesystem or partition UUID or label, because in those cases we can be sure we + * are referring to the right device. */ + + if (PATH_STARTSWITH_SET(ip, + "/dev/disk/by-uuid/", + "/dev/disk/by-partuuid/", + "/dev/disk/by-partlabel/", + "/dev/disk/by-label/")) + return USER_TEST_EXISTS; + + return USER_TEST_MAYBE; + } + + return -EBADFD; + + case USER_CLASSIC: + case USER_DIRECTORY: + case USER_SUBVOLUME: + case USER_FSCRYPT: + if (S_ISDIR(st.st_mode)) + return USER_TEST_EXISTS; + + return -ENOTDIR; + + default: + assert_not_reached("Unexpected record type"); + } +} + +int user_record_test_image_path_and_warn(UserRecord *h) { + int r; + + assert(h); + + r = user_record_test_image_path(h); + if (r == -ENXIO) + return log_error_errno(r, "User record lacks image path, refusing."); + if (r == -EBADFD) + return log_error_errno(r, "Image path %s is not a regular file or block device, refusing.", user_record_image_path(h)); + if (r == -ENOTDIR) + return log_error_errno(r, "Image path %s is not a directory, refusing.", user_record_image_path(h)); + if (r < 0) + return log_error_errno(r, "Failed to test whether image path %s exists: %m", user_record_image_path(h)); + + return r; +} + +int user_record_test_secret(UserRecord *h, UserRecord *secret) { + char **i; + int r; + + assert(h); + + /* Checks whether any of the specified passwords matches any of the hashed passwords of the entry */ + + if (strv_isempty(h->hashed_password)) + return -ENXIO; + + STRV_FOREACH(i, secret->password) { + r = test_password(h->hashed_password, *i); + if (r < 0) + return r; + if (r > 0) + return 0; + } + + return -ENOKEY; +} + +int user_record_set_disk_size(UserRecord *h, uint64_t disk_size) { + _cleanup_(json_variant_unrefp) JsonVariant *new_per_machine = NULL, *midv = NULL, *midav = NULL, *ne = NULL; + _cleanup_free_ JsonVariant **array = NULL; + char smid[SD_ID128_STRING_MAX]; + size_t idx = SIZE_MAX, n; + JsonVariant *per_machine; + sd_id128_t mid; + int r; + + assert(h); + + if (!h->json) + return -EUNATCH; + + if (disk_size < USER_DISK_SIZE_MIN || disk_size > USER_DISK_SIZE_MAX) + return -ERANGE; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + sd_id128_to_string(mid, smid); + + r = json_variant_new_string(&midv, smid); + if (r < 0) + return r; + + r = json_variant_new_array(&midav, (JsonVariant*[]) { midv }, 1); + if (r < 0) + return r; + + per_machine = json_variant_by_key(h->json, "perMachine"); + if (per_machine) { + size_t i; + + if (!json_variant_is_array(per_machine)) + return -EINVAL; + + n = json_variant_elements(per_machine); + + array = new(JsonVariant*, n + 1); + if (!array) + return -ENOMEM; + + for (i = 0; i < n; i++) { + JsonVariant *m; + + array[i] = json_variant_by_index(per_machine, i); + + if (!json_variant_is_object(array[i])) + return -EINVAL; + + m = json_variant_by_key(array[i], "matchMachineId"); + if (!m) { + /* No machineId field? Let's ignore this, but invalidate what we found so far */ + idx = SIZE_MAX; + continue; + } + + if (json_variant_equal(m, midv) || + json_variant_equal(m, midav)) { + /* Matches exactly what we are looking for. Let's use this */ + idx = i; + continue; + } + + r = per_machine_id_match(m, JSON_PERMISSIVE); + if (r < 0) + return r; + if (r > 0) + /* Also matches what we are looking for, but with a broader match. In this + * case let's ignore this entry, and add a new specific one to the end. */ + idx = SIZE_MAX; + } + + if (idx == SIZE_MAX) + idx = n++; /* Nothing suitable found, place new entry at end */ + else + ne = json_variant_ref(array[idx]); + + } else { + array = new(JsonVariant*, 1); + if (!array) + return -ENOMEM; + + idx = 0; + n = 1; + } + + if (!ne) { + r = json_variant_set_field(&ne, "matchMachineId", midav); + if (r < 0) + return r; + } + + r = json_variant_set_field_unsigned(&ne, "diskSize", disk_size); + if (r < 0) + return r; + + assert(idx < n); + array[idx] = ne; + + r = json_variant_new_array(&new_per_machine, array, n); + if (r < 0) + return r; + + r = json_variant_set_field(&h->json, "perMachine", new_per_machine); + if (r < 0) + return r; + + h->disk_size = disk_size; + h->mask |= USER_RECORD_PER_MACHINE; + return 0; +} + +int user_record_update_last_changed(UserRecord *h, bool with_password) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + usec_t n; + int r; + + assert(h); + + if (!h->json) + return -EUNATCH; + + n = now(CLOCK_REALTIME); + + /* refuse downgrading */ + if (h->last_change_usec != UINT64_MAX && h->last_change_usec >= n) + return -ECHRNG; + if (h->last_password_change_usec != UINT64_MAX && h->last_password_change_usec >= n) + return -ECHRNG; + + v = json_variant_ref(h->json); + + r = json_variant_set_field_unsigned(&v, "lastChangeUSec", n); + if (r < 0) + return r; + + if (with_password) { + r = json_variant_set_field_unsigned(&v, "lastPasswordChangeUSec", n); + if (r < 0) + return r; + + h->last_password_change_usec = n; + } + + h->last_change_usec = n; + + json_variant_unref(h->json); + h->json = TAKE_PTR(v); + + h->mask |= USER_RECORD_REGULAR; + return 0; +} + +int user_record_make_hashed_password(UserRecord *h, UserRecord *secret) { + _cleanup_(json_variant_unrefp) JsonVariant *new_array = NULL, *priv = NULL; + _cleanup_strv_free_ char **np = NULL; + char **i; + int r; + + assert(h); + assert(secret); + + /* Initializes the hashed password list from the specified plaintext passwords */ + + STRV_FOREACH(i, secret->password) { + _cleanup_free_ char *salt = NULL; + struct crypt_data cd = {}; + char *k; + + r = make_salt(&salt); + if (r < 0) + return r; + + errno = 0; + k = crypt_r(*i, salt, &cd); + if (!k) + return errno_or_else(EINVAL); + + r = strv_extend(&np, k); + if (r < 0) + return r; + } + + r = json_variant_new_array_strv(&new_array, np); + if (r < 0) + return r; + + priv = json_variant_ref(json_variant_by_key(h->json, "privileged")); + r = json_variant_set_field(&priv, "hashedPassword", new_array); + if (r < 0) + return r; + + r = json_variant_set_field(&h->json, "privileged", priv); + if (r < 0) + return r; + + strv_free(h->hashed_password); + h->hashed_password = TAKE_PTR(np); + + h->mask |= USER_RECORD_PRIVILEGED; + return 0; +} + +int user_record_set_password(UserRecord *h, char **password, bool prepend) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL, *l = NULL; + _cleanup_(strv_free_erasep) char **e = NULL; + int r; + + assert(h); + + if (prepend) { + e = strv_copy(password); + if (!e) + return -ENOMEM; + + r = strv_extend_strv(&e, h->password, true); + if (r < 0) + return r; + + if (strv_equal(h->password, e)) + return 0; + + } else { + if (strv_equal(h->password, password)) + return 0; + + e = strv_copy(password); + if (!e) + return -ENOMEM; + } + + r = json_variant_new_array_strv(&l, e); + if (r < 0) + return r; + + json_variant_sensitive(l); + + w = json_variant_ref(json_variant_by_key(h->json, "secret")); + r = json_variant_set_field(&w, "password", l); + if (r < 0) + return r; + + r = json_variant_set_field(&h->json, "secret", w); + if (r < 0) + return r; + + strv_free_and_replace(h->password, e); + + h->mask |= USER_RECORD_SECRET; + return 0; +} + +int user_record_merge_secret(UserRecord *h, UserRecord *secret) { + assert(h); + + /* Merges the secrets from 'secret' into 'h'. */ + + return user_record_set_password(h, secret->password, true); +} + +int user_record_good_authentication(UserRecord *h) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL; + char buf[SD_ID128_STRING_MAX]; + uint64_t counter, usec; + sd_id128_t mid; + int r; + + assert(h); + + switch (h->good_authentication_counter) { + case UINT64_MAX: + counter = 1; + break; + case UINT64_MAX-1: + counter = h->good_authentication_counter; /* saturate */ + break; + default: + counter = h->good_authentication_counter + 1; + break; + } + + usec = now(CLOCK_REALTIME); + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + v = json_variant_ref(h->json); + w = json_variant_ref(json_variant_by_key(v, "status")); + z = json_variant_ref(json_variant_by_key(w, sd_id128_to_string(mid, buf))); + + r = json_variant_set_field_unsigned(&z, "goodAuthenticationCounter", counter); + if (r < 0) + return r; + + r = json_variant_set_field_unsigned(&z, "lastGoodAuthenticationUSec", usec); + if (r < 0) + return r; + + r = json_variant_set_field(&w, buf, z); + if (r < 0) + return r; + + r = json_variant_set_field(&v, "status", w); + if (r < 0) + return r; + + json_variant_unref(h->json); + h->json = TAKE_PTR(v); + + h->good_authentication_counter = counter; + h->last_good_authentication_usec = usec; + + h->mask |= USER_RECORD_STATUS; + return 0; +} + +int user_record_bad_authentication(UserRecord *h) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL; + char buf[SD_ID128_STRING_MAX]; + uint64_t counter, usec; + sd_id128_t mid; + int r; + + assert(h); + + switch (h->bad_authentication_counter) { + case UINT64_MAX: + counter = 1; + break; + case UINT64_MAX-1: + counter = h->bad_authentication_counter; /* saturate */ + break; + default: + counter = h->bad_authentication_counter + 1; + break; + } + + usec = now(CLOCK_REALTIME); + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + v = json_variant_ref(h->json); + w = json_variant_ref(json_variant_by_key(v, "status")); + z = json_variant_ref(json_variant_by_key(w, sd_id128_to_string(mid, buf))); + + r = json_variant_set_field_unsigned(&z, "badAuthenticationCounter", counter); + if (r < 0) + return r; + + r = json_variant_set_field_unsigned(&z, "lastBadAuthenticationUSec", usec); + if (r < 0) + return r; + + r = json_variant_set_field(&w, buf, z); + if (r < 0) + return r; + + r = json_variant_set_field(&v, "status", w); + if (r < 0) + return r; + + json_variant_unref(h->json); + h->json = TAKE_PTR(v); + + h->bad_authentication_counter = counter; + h->last_bad_authentication_usec = usec; + + h->mask |= USER_RECORD_STATUS; + return 0; +} + +int user_record_ratelimit(UserRecord *h) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL, *z = NULL; + usec_t usec, new_ratelimit_begin_usec, new_ratelimit_count; + char buf[SD_ID128_STRING_MAX]; + sd_id128_t mid; + int r; + + assert(h); + + usec = now(CLOCK_REALTIME); + + if (h->ratelimit_begin_usec != UINT64_MAX && h->ratelimit_begin_usec > usec) + /* Hmm, time is running backwards? Say no! */ + return 0; + else if (h->ratelimit_begin_usec == UINT64_MAX || + usec_add(h->ratelimit_begin_usec, user_record_ratelimit_interval_usec(h)) <= usec) { + /* Fresh start */ + new_ratelimit_begin_usec = usec; + new_ratelimit_count = 1; + } else if (h->ratelimit_count < user_record_ratelimit_burst(h)) { + /* Count up */ + new_ratelimit_begin_usec = h->ratelimit_begin_usec; + new_ratelimit_count = h->ratelimit_count + 1; + } else + /* Limit hit */ + return 0; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + v = json_variant_ref(h->json); + w = json_variant_ref(json_variant_by_key(v, "status")); + z = json_variant_ref(json_variant_by_key(w, sd_id128_to_string(mid, buf))); + + r = json_variant_set_field_unsigned(&z, "rateLimitBeginUSec", new_ratelimit_begin_usec); + if (r < 0) + return r; + + r = json_variant_set_field_unsigned(&z, "rateLimitCount", new_ratelimit_count); + if (r < 0) + return r; + + r = json_variant_set_field(&w, buf, z); + if (r < 0) + return r; + + r = json_variant_set_field(&v, "status", w); + if (r < 0) + return r; + + json_variant_unref(h->json); + h->json = TAKE_PTR(v); + + h->ratelimit_begin_usec = new_ratelimit_begin_usec; + h->ratelimit_count = new_ratelimit_count; + + h->mask |= USER_RECORD_STATUS; + return 1; +} + +int user_record_is_supported(UserRecord *hr, sd_bus_error *error) { + assert(hr); + + if (hr->disposition >= 0 && hr->disposition != USER_REGULAR) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Cannot manage anything but regular users."); + + if (hr->storage >= 0 && !IN_SET(hr->storage, USER_LUKS, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT, USER_CIFS)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User record has storage type this service cannot manage."); + + if (gid_is_valid(hr->gid) && hr->uid != (uid_t) hr->gid) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "User record has to have matching UID/GID fields."); + + if (hr->service && !streq(hr->service, "io.systemd.Home")) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Not accepted with service not matching io.systemd.Home."); + + return 0; +} diff --git a/src/home/user-record-util.h b/src/home/user-record-util.h new file mode 100644 index 0000000000000..ce70b0938d461 --- /dev/null +++ b/src/home/user-record-util.h @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "user-record.h" +#include "group-record.h" + +int user_record_synthesize(UserRecord *h, const char *user_name, const char *realm, const char *image_path, UserStorage storage, uid_t uid, gid_t gid); +int group_record_synthesize(GroupRecord *g, UserRecord *u); + +typedef enum UserReconcileMode { + USER_RECONCILE_ANY, + USER_RECONCILE_REQUIRE_NEWER, /* host version must be newer than embedded version */ + USER_RECONCILE_REQUIRE_NEWER_OR_EQUAL, /* similar, but may also be equal */ + _USER_RECONCILE_MODE_MAX, + _USER_RECONCILE_MODE_INVALID = -1, +} UserReconcileMode; + +enum { /* return values */ + USER_RECONCILE_HOST_WON, + USER_RECONCILE_EMBEDDED_WON, + USER_RECONCILE_IDENTICAL, +}; + +int user_record_reconcile(UserRecord *host, UserRecord *embedded, UserReconcileMode mode, UserRecord **ret); +int user_record_add_binding(UserRecord *h, UserStorage storage, const char *image_path, sd_id128_t partition_uuid, sd_id128_t luks_uuid, sd_id128_t fs_uuid, const char *luks_cipher, const char *luks_cipher_mode, uint64_t luks_volume_key_size, const char *file_system_type, const void *fscrypt_salt, size_t fscrypt_salt_size, const char *home_directory, uid_t uid, gid_t gid); + +/* Results of the two test functions below. */ +enum { + USER_TEST_UNDEFINED, /* Returned by user_record_test_image_path() if the storage type knows no image paths */ + USER_TEST_ABSENT, + USER_TEST_EXISTS, + USER_TEST_MOUNTED, /* Only applies to user_record_test_home_directory(), when the home directory exists. */ + USER_TEST_MAYBE, /* Only applies to LUKS devices: block device exists, but we don't know if it's the right one */ +}; + +int user_record_test_home_directory(UserRecord *h); +int user_record_test_home_directory_and_warn(UserRecord *h); +int user_record_test_image_path(UserRecord *h); +int user_record_test_image_path_and_warn(UserRecord *h); + +int user_record_test_secret(UserRecord *h, UserRecord *secret); + +int user_record_update_last_changed(UserRecord *h, bool with_password); +int user_record_set_disk_size(UserRecord *h, uint64_t disk_size); +int user_record_set_password(UserRecord *h, char **password, bool prepend); +int user_record_make_hashed_password(UserRecord *h, UserRecord *secret); +int user_record_merge_secret(UserRecord *h, UserRecord *secret); + +int user_record_good_authentication(UserRecord *h); +int user_record_bad_authentication(UserRecord *h); +int user_record_ratelimit(UserRecord *h); + +int user_record_is_supported(UserRecord *hr, sd_bus_error *error); diff --git a/src/libsystemd/libsystemd.sym b/src/libsystemd/libsystemd.sym index 5ec42e0f1f826..13cca2db6c46e 100644 --- a/src/libsystemd/libsystemd.sym +++ b/src/libsystemd/libsystemd.sym @@ -682,3 +682,8 @@ global: sd_bus_object_vtable_format; sd_event_source_disable_unref; } LIBSYSTEMD_241; + +LIBSYSTEMD_244 { +global: + sd_bus_message_sensitive; +} LIBSYSTEMD_243; diff --git a/src/libsystemd/sd-bus/bus-common-errors.c b/src/libsystemd/sd-bus/bus-common-errors.c index edd30bf84d8ad..8f7a79fb39b70 100644 --- a/src/libsystemd/sd-bus/bus-common-errors.c +++ b/src/libsystemd/sd-bus/bus-common-errors.c @@ -104,5 +104,28 @@ BUS_ERROR_MAP_ELF_REGISTER const sd_bus_error_map bus_common_errors[] = { SD_BUS_ERROR_MAP(BUS_ERROR_SPEED_METER_INACTIVE, EOPNOTSUPP), + SD_BUS_ERROR_MAP(BUS_ERROR_NO_SUCH_HOME, EEXIST), + SD_BUS_ERROR_MAP(BUS_ERROR_UID_IN_USE, EEXIST), + SD_BUS_ERROR_MAP(BUS_ERROR_USER_NAME_EXISTS, EEXIST), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_EXISTS, EEXIST), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_ALREADY_ACTIVE, EALREADY), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_ALREADY_FIXATED, EALREADY), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_UNFIXATED, EADDRNOTAVAIL), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_NOT_ACTIVE, EALREADY), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_ABSENT, EREMOTE), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_BUSY, EBUSY), + SD_BUS_ERROR_MAP(BUS_ERROR_BAD_PASSWORD, ENOKEY), + SD_BUS_ERROR_MAP(BUS_ERROR_LOW_PASSWORD_QUALITY, EUCLEAN), + SD_BUS_ERROR_MAP(BUS_ERROR_BAD_SIGNATURE, EKEYREJECTED), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_RECORD_MISMATCH, EUCLEAN), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_RECORD_DOWNGRADE, ESTALE), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_RECORD_SIGNED, EROFS), + SD_BUS_ERROR_MAP(BUS_ERROR_BAD_HOME_SIZE, ERANGE), + SD_BUS_ERROR_MAP(BUS_ERROR_NO_PRIVATE_KEY, ENOPKG), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_LOCKED, ENOEXEC), + SD_BUS_ERROR_MAP(BUS_ERROR_HOME_NOT_LOCKED, ENOEXEC), + SD_BUS_ERROR_MAP(BUS_ERROR_TOO_MANY_OPERATIONS, ENOBUFS), + SD_BUS_ERROR_MAP(BUS_ERROR_AUTHENTICATION_LIMIT_HIT, ETOOMANYREFS), + SD_BUS_ERROR_MAP_END }; diff --git a/src/libsystemd/sd-bus/bus-common-errors.h b/src/libsystemd/sd-bus/bus-common-errors.h index 4a29b3bea8ecb..d3f002d75a7d5 100644 --- a/src/libsystemd/sd-bus/bus-common-errors.h +++ b/src/libsystemd/sd-bus/bus-common-errors.h @@ -83,4 +83,28 @@ #define BUS_ERROR_SPEED_METER_INACTIVE "org.freedesktop.network1.SpeedMeterInactive" +#define BUS_ERROR_NO_SUCH_HOME "org.freedesktop.home1.NoSuchHome" +#define BUS_ERROR_UID_IN_USE "org.freedesktop.home1.UIDInUse" +#define BUS_ERROR_USER_NAME_EXISTS "org.freedesktop.home1.UserNameExists" +#define BUS_ERROR_HOME_EXISTS "org.freedesktop.home1.HomeExists" +#define BUS_ERROR_HOME_ALREADY_ACTIVE "org.freedesktop.home1.HomeAlreadyActive" +#define BUS_ERROR_HOME_ALREADY_FIXATED "org.freedesktop.home1.HomeAlreadyFixated" +#define BUS_ERROR_HOME_UNFIXATED "org.freedesktop.home1.HomeUnfixated" +#define BUS_ERROR_HOME_NOT_ACTIVE "org.freedesktop.home1.HomeNotActive" +#define BUS_ERROR_HOME_ABSENT "org.freedesktop.home1.HomeAbsent" +#define BUS_ERROR_HOME_BUSY "org.freedesktop.home1.HomeBusy" +#define BUS_ERROR_BAD_PASSWORD "org.freedesktop.home1.BadPassword" +#define BUS_ERROR_LOW_PASSWORD_QUALITY "org.freedesktop.home1.LowPasswordQuality" +#define BUS_ERROR_BAD_SIGNATURE "org.freedesktop.home1.BadSignature" +#define BUS_ERROR_HOME_RECORD_MISMATCH "org.freedesktop.home1.RecordMismatch" +#define BUS_ERROR_HOME_RECORD_DOWNGRADE "org.freedesktop.home1.RecordDowngrade" +#define BUS_ERROR_HOME_RECORD_SIGNED "org.freedesktop.home1.RecordSigned" +#define BUS_ERROR_BAD_HOME_SIZE "org.freedesktop.home1.BadHomeSize" +#define BUS_ERROR_NO_PRIVATE_KEY "org.freedesktop.home1.NoPrivateKey" +#define BUS_ERROR_HOME_LOCKED "org.freedesktop.home1.HomeLocked" +#define BUS_ERROR_HOME_NOT_LOCKED "org.freedesktop.home1.HomeNotLocked" +#define BUS_ERROR_NO_DISK_SPACE "org.freedesktop.home1.NoDiskSpace" +#define BUS_ERROR_TOO_MANY_OPERATIONS "org.freedesktop.home1.TooManyOperations" +#define BUS_ERROR_AUTHENTICATION_LIMIT_HIT "org.freedesktop.home1.AuthenticationLimitHit" + BUS_ERROR_MAP_ELF_USE(bus_common_errors); diff --git a/src/libsystemd/sd-bus/bus-message.c b/src/libsystemd/sd-bus/bus-message.c index eb029e4453268..0515064236a5d 100644 --- a/src/libsystemd/sd-bus/bus-message.c +++ b/src/libsystemd/sd-bus/bus-message.c @@ -45,12 +45,24 @@ static void message_free_part(sd_bus_message *m, struct bus_body_part *part) { assert(m); assert(part); - if (part->memfd >= 0) + if (part->memfd >= 0) { + /* erase this requested, but ony if the memfd is not sealed yet, i.e. is writable */ + if (m->sensitive && !m->sealed) + explicit_bzero_safe(part->data, part->size); + close_and_munmap(part->memfd, part->mmap_begin, part->mapped); - else if (part->munmap_this) + } else if (part->munmap_this) + /* We don't erase sensitive data here, since the data is memory mapped from someone else, and + * we just don't know if it's OK to write to it */ munmap(part->mmap_begin, part->mapped); - else if (part->free_this) - free(part->data); + else { + /* Erase this if that is requested. Since this is regular memory we know we can write it. */ + if (m->sensitive) + explicit_bzero_safe(part->data, part->size); + + if (part->free_this) + free(part->data); + } if (part != &m->body) free(part); @@ -113,11 +125,11 @@ static void message_reset_containers(sd_bus_message *m) { static sd_bus_message* message_free(sd_bus_message *m) { assert(m); + message_reset_parts(m); + if (m->free_header) free(m->header); - message_reset_parts(m); - /* Note that we don't unref m->bus here. That's already done by sd_bus_message_unref() as each user * reference to the bus message also is considered a reference to the bus connection itself. */ @@ -727,6 +739,12 @@ static int message_new_reply( t->dont_send = !!(call->header->flags & BUS_MESSAGE_NO_REPLY_EXPECTED); t->enforced_reply_signature = call->enforced_reply_signature; + /* let's copy the sensitive flag over. Let's do that as a safety precaution to keep a transaction + * wholly sensitive if already the incoming message was sensitive. This is particularly useful when a + * vtable record sets the SD_BUS_VTABLE_SENSITIVE flag on a method call, since this means it applies + * to both the message call and the reply. */ + t->sensitive = call->sensitive; + *m = TAKE_PTR(t); return 0; } @@ -5919,3 +5937,10 @@ _public_ int sd_bus_message_set_priority(sd_bus_message *m, int64_t priority) { m->priority = priority; return 0; } + +_public_ int sd_bus_message_sensitive(sd_bus_message *m) { + assert_return(m, -EINVAL); + + m->sensitive = true; + return 0; +} diff --git a/src/libsystemd/sd-bus/bus-message.h b/src/libsystemd/sd-bus/bus-message.h index ced0bb3d34a91..a88a531e15b82 100644 --- a/src/libsystemd/sd-bus/bus-message.h +++ b/src/libsystemd/sd-bus/bus-message.h @@ -85,6 +85,7 @@ struct sd_bus_message { bool free_header:1; bool free_fds:1; bool poisoned:1; + bool sensitive:1; /* The first and last bytes of the message */ struct bus_header *header; diff --git a/src/libsystemd/sd-bus/bus-objects.c b/src/libsystemd/sd-bus/bus-objects.c index ae643cacc740f..474f8a6a1695c 100644 --- a/src/libsystemd/sd-bus/bus-objects.c +++ b/src/libsystemd/sd-bus/bus-objects.c @@ -353,6 +353,12 @@ static int method_callbacks_run( if (require_fallback && !c->parent->is_fallback) return 0; + if (FLAGS_SET(c->vtable->flags, SD_BUS_VTABLE_SENSITIVE)) { + r = sd_bus_message_sensitive(m); + if (r < 0) + return r; + } + r = check_access(bus, m, c, &error); if (r < 0) return bus_maybe_reply_error(m, r, &error); @@ -577,6 +583,12 @@ static int property_get_set_callbacks_run( if (require_fallback && !c->parent->is_fallback) return 0; + if (FLAGS_SET(c->vtable->flags, SD_BUS_VTABLE_SENSITIVE)) { + r = sd_bus_message_sensitive(m); + if (r < 0) + return r; + } + r = vtable_property_get_userdata(bus, m->path, c, &u, &error); if (r <= 0) return bus_maybe_reply_error(m, r, &error); @@ -591,6 +603,12 @@ static int property_get_set_callbacks_run( if (r < 0) return r; + if (FLAGS_SET(c->vtable->flags, SD_BUS_VTABLE_SENSITIVE)) { + r = sd_bus_message_sensitive(reply); + if (r < 0) + return r; + } + if (is_get) { /* Note that we do not protect against reexecution * here (using the last_iteration check, see below), @@ -692,6 +710,12 @@ static int vtable_append_one_property( assert(c); assert(v); + if (FLAGS_SET(c->vtable->flags, SD_BUS_VTABLE_SENSITIVE)) { + r = sd_bus_message_sensitive(reply); + if (r < 0) + return r; + } + r = sd_bus_message_open_container(reply, 'e', "sv"); if (r < 0) return r; @@ -750,9 +774,18 @@ static int vtable_append_all_properties( if (v->flags & SD_BUS_VTABLE_HIDDEN) continue; + /* Let's not include properties marked as "explicit" in any message that contians a generic + * dump of properties, but only in those genrated as a response to an explicit request. */ if (v->flags & SD_BUS_VTABLE_PROPERTY_EXPLICIT) continue; + /* Let's not include properties marked only for invalidation on change (i.e. in contrast to + * those whose new values are included in PropertiesChanges message) in any signals. This is + * useful to ensure they aren't included in InterfacesAdded messages. */ + if (reply->header->type != SD_BUS_MESSAGE_METHOD_RETURN && + FLAGS_SET(v->flags, SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION)) + continue; + r = vtable_append_one_property(bus, reply, path, c, v, userdata, error); if (r < 0) return r; diff --git a/src/login/inhibit.c b/src/login/inhibit.c index 25ff4d0be6428..b3b37fb595906 100644 --- a/src/login/inhibit.c +++ b/src/login/inhibit.c @@ -113,9 +113,9 @@ static int print_inhibitors(sd_bus *bus) { r = table_add_many(table, TABLE_STRING, who, - TABLE_UINT32, uid, + TABLE_UID, (uid_t) uid, TABLE_STRING, strna(u), - TABLE_UINT32, pid, + TABLE_PID, (pid_t) pid, TABLE_STRING, strna(comm), TABLE_STRING, what, TABLE_STRING, why, diff --git a/src/login/loginctl.c b/src/login/loginctl.c index 2ad9887066f6c..6e7d6600132f4 100644 --- a/src/login/loginctl.c +++ b/src/login/loginctl.c @@ -185,7 +185,7 @@ static int list_sessions(int argc, char *argv[], void *userdata) { r = table_add_many(table, TABLE_STRING, id, - TABLE_UINT32, uid, + TABLE_UID, (uid_t) uid, TABLE_STRING, user, TABLE_STRING, seat, TABLE_STRING, strna(tty)); @@ -244,7 +244,7 @@ static int list_users(int argc, char *argv[], void *userdata) { break; r = table_add_many(table, - TABLE_UINT32, uid, + TABLE_UID, (uid_t) uid, TABLE_STRING, user); if (r < 0) return log_error_errno(r, "Failed to add row to table: %m"); diff --git a/src/login/logind-core.c b/src/login/logind-core.c index 1d21e90a2eaa9..7a29ccdf91ebd 100644 --- a/src/login/logind-core.c +++ b/src/login/logind-core.c @@ -28,6 +28,7 @@ #include "terminal-util.h" #include "udev-util.h" #include "user-util.h" +#include "userdb.h" void manager_reset_config(Manager *m) { assert(m); @@ -139,21 +140,18 @@ int manager_add_session(Manager *m, const char *id, Session **_session) { int manager_add_user( Manager *m, - uid_t uid, - gid_t gid, - const char *name, - const char *home, + UserRecord *ur, User **_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; } @@ -169,32 +167,35 @@ int manager_add_user_by_name( const char *name, User **_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, _user); + return manager_add_user(m, ur, _user); } -int manager_add_user_by_uid(Manager *m, uid_t uid, User **_user) { - struct passwd *p; +int manager_add_user_by_uid( + Manager *m, + uid_t uid, + User **_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, _user); + return manager_add_user(m, ur, _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 b36616e55a03b..695e6dfbe21ea 100644 --- a/src/login/logind-dbus.c +++ b/src/login/logind-dbus.c @@ -540,8 +540,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) @@ -581,8 +581,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; @@ -1491,7 +1491,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 094fcd668dcb1..a2b4cac976d03 100644 --- a/src/login/logind-seat.c +++ b/src/login/logind-seat.c @@ -120,7 +120,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) { @@ -138,7 +138,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'); } @@ -217,8 +217,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 a37bbf56b75ad..59e6fe40091fd 100644 --- a/src/login/logind-session-dbus.c +++ b/src/login/logind-session-dbus.c @@ -44,7 +44,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( @@ -62,7 +62,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( @@ -169,7 +169,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) @@ -211,7 +211,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) @@ -247,7 +247,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"); session_set_idle_hint(s, b); @@ -276,7 +276,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); @@ -315,7 +315,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) @@ -351,7 +351,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); @@ -520,7 +520,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); @@ -821,7 +821,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 : "", @@ -833,7 +833,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 7e8025a0ea22b..e962fe60e2410 100644 --- a/src/login/logind-session.c +++ b/src/login/logind-session.c @@ -231,8 +231,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)), @@ -642,7 +642,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, @@ -658,7 +658,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); @@ -699,9 +699,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); @@ -751,7 +751,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) { @@ -767,7 +770,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)); } @@ -825,7 +828,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)); @@ -1194,7 +1197,7 @@ 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 beb97362e7302..6af226d006e7e 100644 --- a/src/login/logind-user-dbus.c +++ b/src/login/logind-user-dbus.c @@ -16,6 +16,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( @@ -153,7 +207,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) @@ -182,7 +236,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) @@ -207,9 +261,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), @@ -279,7 +333,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; @@ -348,7 +402,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 b17fb2e3225e6..903b9f0cb84b8 100644 --- a/src/login/logind-user.c +++ b/src/login/logind-user.c @@ -38,10 +38,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]; @@ -49,7 +46,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) @@ -57,28 +60,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) - return -ENOMEM; - - u->home = strdup(home); - if (!u->home) + if (asprintf(&u->state_file, "/run/systemd/users/" 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) + if (asprintf(&u->runtime_path, "/run/user/" UID_FMT, ur->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; @@ -91,7 +83,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; @@ -130,9 +122,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); @@ -141,8 +133,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); } @@ -170,7 +162,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)); @@ -364,6 +356,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_error_errno(sd_bus_message_get_errno(m), + "Failed to update slice of %s: %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_high != 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); @@ -377,12 +463,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); @@ -461,7 +550,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); @@ -475,8 +564,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; } @@ -532,7 +621,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; @@ -568,6 +657,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; @@ -580,12 +681,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. */ } @@ -713,7 +818,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)) { @@ -738,6 +843,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); @@ -756,7 +862,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) { @@ -767,7 +874,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"); @@ -776,8 +883,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 4bd65d8373414..f8f172cb0f6d5 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 f260f2dc96de8..65bcf7c5fbc9f 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; @@ -131,7 +132,7 @@ int manager_add_device(Manager *m, const char *sysfs, bool master, Device **_dev int manager_add_button(Manager *m, const char *name, Button **_button); int manager_add_seat(Manager *m, const char *id, Seat **_seat); int manager_add_session(Manager *m, const char *id, Session **_session); -int manager_add_user(Manager *m, uid_t uid, gid_t gid, const char *name, const char *home, User **_user); +int manager_add_user(Manager *m, UserRecord *ur, User **_user); int manager_add_user_by_name(Manager *m, const char *name, User **_user); int manager_add_user_by_uid(Manager *m, uid_t uid, User **_user); int manager_add_inhibitor(Manager *m, const char* id, Inhibitor **_inhibitor); diff --git a/src/login/org.freedesktop.login1.policy b/src/login/org.freedesktop.login1.policy index 6dc79aa32aa4d..b9a0b68dfbf2c 100644 --- a/src/login/org.freedesktop.login1.policy +++ b/src/login/org.freedesktop.login1.policy @@ -2,16 +2,7 @@ - + diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 3f762cbbc30b4..261b738564163 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -25,16 +25,24 @@ #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) static int parse_argv( pam_handle_t *handle, @@ -50,28 +58,30 @@ static int parse_argv( assert(argc == 0 || argv); for (i = 0; i < (unsigned) argc; i++) { - if (startswith(argv[i], "class=")) { + const char *p; + + if ((p = startswith(argv[i], "class="))) { if (class) - *class = argv[i] + 6; + *class = p; - } else if (startswith(argv[i], "type=")) { + } else if ((p = startswith(argv[i], "type="))) { if (type) - *type = argv[i] + 5; + *type = p; - } else if (startswith(argv[i], "desktop=")) { + } else if ((p = startswith(argv[i], "desktop="))) { if (desktop) - *desktop = argv[i] + 8; + *desktop = p; } else if (streq(argv[i], "debug")) { if (debug) *debug = true; - } else if (startswith(argv[i], "debug=")) { + } else if ((p = startswith(argv[i], "debug="))) { int k; - k = parse_boolean(argv[i] + 6); + k = parse_boolean(p); if (k < 0) - pam_syslog(handle, LOG_WARNING, "Failed to parse debug= argument, ignoring."); + pam_syslog(handle, LOG_WARNING, "Failed to parse debug= argument, ignoring: %s", p); else if (debug) *debug = k; @@ -82,38 +92,93 @@ 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) { - pam_syslog(handle, LOG_ERR, "Failed to get user name."); + pam_syslog(handle, LOG_ERR, "Failed to get user name: %s", pam_strerror(handle, r)); return r; } 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; } @@ -229,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) { @@ -247,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_tasks_max(pam_handle_t *handle, sd_bus_message *m, const char *limit) { @@ -285,19 +348,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) { @@ -305,21 +366,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) { @@ -362,7 +421,7 @@ static int update_environment(pam_handle_t *handle, const char *key, const char r = pam_misc_setenv(handle, key, value, 0); if (r != PAM_SUCCESS) - pam_syslog(handle, LOG_ERR, "Failed to set environment variable %s.", key); + pam_syslog(handle, LOG_ERR, "Failed to set environment variable %s: %s", key, pam_strerror(handle, r)); return r; } @@ -370,6 +429,7 @@ static int update_environment(pam_handle_t *handle, const char *key, const char static bool validate_runtime_directory(pam_handle_t *handle, const char *path, uid_t uid) { struct stat st; + assert(handle); assert(path); /* Just some extra paranoia: let's not set $XDG_RUNTIME_DIR if the directory we'd set it to isn't actually set @@ -397,6 +457,140 @@ static bool validate_runtime_directory(pam_handle_t *handle, const char *path, u return false; } +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(handle, *i); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable $EMAIL: %s", pam_strerror(handle, r)); + return r; + } + + if (debug) + pam_syslog(handle, LOG_DEBUG, "PAM environment variable %s set, based on user record.", *i); + } + + 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(handle, joined); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable $EMAIL: %s", pam_strerror(handle, r)); + return r; + } + + if (debug) + pam_syslog(handle, LOG_DEBUG, "PAM environment variable $EMAIL set, based on user record."); + } + } + + 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(handle, joined); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable $TZ: %s", pam_strerror(handle, r)); + return r; + } + + if (debug) + pam_syslog(handle, LOG_DEBUG, "PAM environment variable $TZ set, based on user record."); + } + } + + 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(handle, joined); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable $LANG: %s", pam_strerror(handle, r)); + return r; + } + + if (debug) + pam_syslog(handle, LOG_DEBUG, "PAM environment variable $LANG set, based on user record."); + } + } + + 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(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(errno)); + 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, @@ -405,7 +599,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, @@ -414,9 +608,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; _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; @@ -437,11 +631,9 @@ _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); - if (r != PAM_SUCCESS) { - pam_syslog(handle, LOG_ERR, "Failed to get user data."); + r = acquire_user_record(handle, &ur); + if (r != PAM_SUCCESS) return r; - } /* Make sure we don't enter a loop by talking to * systemd-logind when it is actually waiting for the @@ -449,20 +641,24 @@ _public_ PAM_EXTERN int pam_sm_open_session( * "systemd-user" we simply set XDG_RUNTIME_DIR and * leave. */ - pam_get_item(handle, PAM_SERVICE, (const void**) &service); + (void) pam_get_item(handle, PAM_SERVICE, (const void**) &service); 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."); + pam_syslog(handle, LOG_ERR, "Failed to set runtime dir: %s", pam_strerror(handle, r)); return r; } } - 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; @@ -471,10 +667,10 @@ _public_ PAM_EXTERN int pam_sm_open_session( /* Otherwise, we ask logind to create a session for us */ - pam_get_item(handle, PAM_XDISPLAY, (const void**) &display); - pam_get_item(handle, PAM_TTY, (const void**) &tty); - pam_get_item(handle, PAM_RUSER, (const void**) &remote_user); - pam_get_item(handle, PAM_RHOST, (const void**) &remote_host); + (void) pam_get_item(handle, PAM_XDISPLAY, (const void**) &display); + (void) pam_get_item(handle, PAM_TTY, (const void**) &tty); + (void) pam_get_item(handle, PAM_RUSER, (const void**) &remote_user); + (void) pam_get_item(handle, PAM_RHOST, (const void**) &remote_host); seat = getenv_harder(handle, "XDG_SEAT", NULL); cvtnr = getenv_harder(handle, "XDG_VTNR", NULL); @@ -548,16 +744,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), @@ -574,13 +768,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, @@ -593,40 +785,34 @@ _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; + return r; r = append_session_tasks_max(handle, m, tasks_max); if (r < 0) - return PAM_SESSION_ERR; + return r; r = append_session_cg_weight(handle, m, cpu_weight, "CPUWeight"); if (r < 0) - return PAM_SESSION_ERR; + return r; r = append_session_cg_weight(handle, m, io_weight, "IOWeight"); if (r < 0) - return PAM_SESSION_ERR; + 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, 0, &error, &reply); + r = sd_bus_call(bus, m, LOGIN_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply); if (r < 0) { if (sd_bus_error_has_name(&error, BUS_ERROR_SESSION_BUSY)) { if (debug) @@ -634,7 +820,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( return PAM_SUCCESS; } else { pam_syslog(handle, LOG_ERR, "Failed to create session: %s", bus_error_message(&error, r)); - return PAM_SYSTEM_ERR; + return PAM_SESSION_ERR; } } @@ -648,10 +834,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: " @@ -662,20 +846,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; } @@ -711,7 +895,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( r = pam_set_data(handle, "systemd.existing", INT_TO_PTR(!!existing), NULL); if (r != PAM_SUCCESS) { - pam_syslog(handle, LOG_ERR, "Failed to install existing flag."); + pam_syslog(handle, LOG_ERR, "Failed to install existing flag: %s", pam_strerror(handle, r)); return r; } @@ -724,12 +908,20 @@ _public_ PAM_EXTERN int pam_sm_open_session( r = pam_set_data(handle, "systemd.session-fd", FD_TO_PTR(session_fd), NULL); if (r != PAM_SUCCESS) { - pam_syslog(handle, LOG_ERR, "Failed to install session fd."); + pam_syslog(handle, LOG_ERR, "Failed to install session fd: %s", pam_strerror(handle, r)); safe_close(session_fd); return r; } } + 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 process 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; } @@ -738,8 +930,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; @@ -752,17 +942,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", @@ -779,11 +967,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/network/netdev/macsec.c b/src/network/netdev/macsec.c index cf281e75a6d45..56240b9586a22 100644 --- a/src/network/netdev/macsec.c +++ b/src/network/netdev/macsec.c @@ -981,7 +981,7 @@ static int macsec_read_key_file(NetDev *netdev, SecurityAssociation *sa) { if (!sa->key_file) return 0; - r = read_full_file_full(sa->key_file, READ_FULL_FILE_SECURE | READ_FULL_FILE_UNHEX, (char **) &key, &key_len); + r = read_full_file_full(AT_FDCWD, sa->key_file, READ_FULL_FILE_SECURE | READ_FULL_FILE_UNHEX, (char **) &key, &key_len); if (r < 0) return log_netdev_error_errno(netdev, r, "Failed to read key from '%s', ignoring: %m", diff --git a/src/network/netdev/wireguard.c b/src/network/netdev/wireguard.c index 913ee2a058977..38f47a36f8ea1 100644 --- a/src/network/netdev/wireguard.c +++ b/src/network/netdev/wireguard.c @@ -901,7 +901,7 @@ static int wireguard_read_key_file(const char *filename, uint8_t dest[static WG_ assert(dest); - r = read_full_file_full(filename, READ_FULL_FILE_SECURE | READ_FULL_FILE_UNBASE64, &key, &key_len); + r = read_full_file_full(AT_FDCWD, filename, READ_FULL_FILE_SECURE | READ_FULL_FILE_UNBASE64, &key, &key_len); if (r < 0) return r; diff --git a/src/nspawn/nspawn-oci.c b/src/nspawn/nspawn-oci.c index 4519c74b95b35..782c03c539bc6 100644 --- a/src/nspawn/nspawn-oci.c +++ b/src/nspawn/nspawn-oci.c @@ -172,24 +172,13 @@ static int oci_env(const char *name, JsonVariant *v, JsonDispatchFlags flags, vo static int oci_args(const char *name, JsonVariant *v, JsonDispatchFlags flags, void *userdata) { _cleanup_strv_free_ char **l = NULL; char ***value = userdata; - JsonVariant *e; int r; assert(value); - JSON_VARIANT_ARRAY_FOREACH(e, v) { - const char *n; - - if (!json_variant_is_string(e)) - return json_log(v, flags, SYNTHETIC_ERRNO(EINVAL), - "Argument is not a string."); - - assert_se(n = json_variant_string(e)); - - r = strv_extend(&l, n); - if (r < 0) - return log_oom(); - } + r = json_variant_strv(v, &l); + if (r < 0) + return json_log(v, flags, r, "Cannot parse arguments as list of strings: %m"); if (strv_isempty(l)) return json_log(v, flags, SYNTHETIC_ERRNO(EINVAL), @@ -2214,7 +2203,7 @@ int oci_load(FILE *f, const char *bundle, Settings **ret) { path = strjoina(bundle, "/config.json"); - r = json_parse_file(f, path, &oci, &line, &column); + r = json_parse_file(f, path, 0, &oci, &line, &column); if (r < 0) { if (line != 0 && column != 0) return log_error_errno(r, "Failed to parse '%s' at %u:%u: %m", path, line, column); diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 2aec8041f0079..a96ee1ccc2af7 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -4967,7 +4967,7 @@ static int run(int argc, char *argv[]) { goto finish; } - r = loop_device_make_by_path(arg_image, arg_read_only ? O_RDONLY : O_RDWR, &loop); + r = loop_device_make_by_path(arg_image, arg_read_only ? O_RDONLY : O_RDWR, LO_FLAGS_PARTSCAN, &loop); if (r < 0) { log_error_errno(r, "Failed to set up loopback block device: %m"); goto finish; diff --git a/src/nss-systemd/nss-systemd.c b/src/nss-systemd/nss-systemd.c index 8ef1cd5ea9e74..ae95bba78318b 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,33 @@ static const struct group nobody_group = { .gr_mem = (char*[]) { NULL }, }; -typedef struct UserEntry UserEntry; -typedef struct GetentData GetentData; +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; -struct UserEntry { - uid_t id; - char *name; + /* Applies to group iterations only: this is false while we iterate through groups natviely + * defined. It's true when we iterate through all memberships that extend groups that are + * non-natively (i.e. in NSS defined). */ + bool by_membership; +} GetentData; - GetentData *data; - LIST_FIELDS(UserEntry, entries); +static GetentData getpwent_data = { + .mutex = PTHREAD_MUTEX_INITIALIZER }; -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. */ - pthread_mutex_t mutex; - - UserEntry *position; - LIST_HEAD(UserEntry, entries); +static GetentData getgrent_data = { + .mutex = PTHREAD_MUTEX_INITIALIZER }; -static GetentData getpwent_data = { PTHREAD_MUTEX_INITIALIZER, NULL, NULL }; -static GetentData getgrent_data = { PTHREAD_MUTEX_INITIALIZER, NULL, NULL }; - 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 +83,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)) { - *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) - 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)) + if (streq(name, nobody_passwd.pw_name)) { + if (!synthesize_nobody()) return NSS_STATUS_NOTFOUND; - goto fail; + *pwd = nobody_passwd; + return NSS_STATUS_SUCCESS; } - r = sd_bus_message_read(reply, "u", &translated); - if (r < 0) - goto fail; - } + } else if (STR_IN_SET(name, root_passwd.pw_name, nobody_passwd.pw_name)) + return NSS_STATUS_NOTFOUND; - 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 +134,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) { - *pwd = nobody_passwd; - return NSS_STATUS_SUCCESS; - } - } - if (!uid_is_dynamic(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)) + if (uid == nobody_passwd.pw_uid) { + if (!synthesize_nobody()) return NSS_STATUS_NOTFOUND; - goto fail; + *pwd = nobody_passwd; + return NSS_STATUS_SUCCESS; } - r = sd_bus_message_read(reply, "s", &translated); - if (r < 0) - goto fail; - } + } else if (uid == root_passwd.pw_uid || uid == nobody_passwd.pw_uid) + return NSS_STATUS_NOTFOUND; - 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 +183,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)) { - *gr = nobody_group; - return NSS_STATUS_SUCCESS; - } - } - - 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_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)) + if (streq(name, nobody_group.gr_name)) { + if (!synthesize_nobody()) return NSS_STATUS_NOTFOUND; - goto fail; + *gr = nobody_group; + return NSS_STATUS_SUCCESS; } - r = sd_bus_message_read(reply, "u", &translated); - if (r < 0) - goto fail; - } + } else if (STR_IN_SET(name, root_group.gr_name, nobody_group.gr_name)) + return NSS_STATUS_NOTFOUND; - 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 +231,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) { - *gr = nobody_group; - return NSS_STATUS_SUCCESS; - } - } - - if (!gid_is_dynamic(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)) + if (gid == nobody_group.gr_gid) { + if (!synthesize_nobody()) return NSS_STATUS_NOTFOUND; - goto fail; + *gr = nobody_group; + return NSS_STATUS_SUCCESS; } - r = sd_bus_message_read(reply, "s", &translated); - if (r < 0) - goto fail; - } + } else if (gid == root_group.gr_gid || gid == nobody_group.gr_gid) + return NSS_STATUS_NOTFOUND; - 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 +294,362 @@ 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; +enum nss_status _nss_systemd_setpwent(int stayopen) { + enum nss_status ret; - assert(p); + PROTECT_ERRNO; + BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); - d = opendir("/run/systemd/dynamic-uid/"); - if (!d) - return -errno; + if (userdb_nss_compat_is_enabled() <= 0) + return NSS_STATUS_UNAVAIL; - FOREACH_DIRENT(de, d, return -errno) { - _cleanup_free_ char *name = NULL; - uid_t uid, verified; + assert_se(pthread_mutex_lock(&getpwent_data.mutex) == 0); - if (!dirent_is_file(de)) - continue; + getpwent_data.iterator = userdb_iterator_free(getpwent_data.iterator); + getpwent_data.by_membership = false; - r = parse_uid(de->d_name, &uid); - if (r < 0) - continue; + ret = userdb_all(nss_glue_userdb_flags(), &getpwent_data.iterator) < 0 ? + NSS_STATUS_UNAVAIL : NSS_STATUS_SUCCESS; - r = direct_lookup_uid(uid, &name); - if (r == -ENOMEM) - return r; - if (r < 0) - continue; + assert_se(pthread_mutex_unlock(&getpwent_data.mutex) == 0); + return ret; +} - r = direct_lookup_name(name, &verified); - if (r < 0) - continue; +enum nss_status _nss_systemd_setgrent(int stayopen) { + enum nss_status ret; - if (uid != verified) - continue; + PROTECT_ERRNO; + BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); - r = user_entry_add(p, name, uid); - if (r == -ENOMEM) - return r; - if (r < 0) - continue; - } + if (userdb_nss_compat_is_enabled() <= 0) + return NSS_STATUS_UNAVAIL; + + 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; - return 0; + assert_se(pthread_mutex_unlock(&getgrent_data.mutex) == 0); + return ret; } -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; +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(p); + assert(result); + assert(errnop); - assert_se(pthread_mutex_lock(&p->mutex) == 0); + r = userdb_nss_compat_is_enabled(); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + if (!r) { + UNPROTECT_ERRNO; + *errnop = EHOSTDOWN; + return NSS_STATUS_UNAVAIL; + } - systemd_endent(p); + assert_se(pthread_mutex_lock(&getpwent_data.mutex) == 0); - if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0) + if (!getpwent_data.iterator) { + UNPROTECT_ERRNO; + *errnop = EHOSTDOWN; + ret = NSS_STATUS_UNAVAIL; 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; - + 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 = 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; + r = nss_pack_user_record(ur, result, buffer, buflen); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + ret = NSS_STATUS_TRYAGAIN; + goto finish; } - if (r < 0) - goto fail; - r = sd_bus_message_exit_container(reply); - if (r < 0) - goto fail; + ret = NSS_STATUS_SUCCESS; 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); + assert_se(pthread_mutex_unlock(&getpwent_data.mutex) == 0); + return ret; } -enum nss_status _nss_systemd_setgrent(int stayopen) { - return systemd_setent(&getgrent_data); -} +enum nss_status _nss_systemd_getgrent_r( + struct group *result, + char *buffer, size_t buflen, + int *errnop) { -enum nss_status _nss_systemd_getpwent_r(struct passwd *result, char *buffer, size_t buflen, int *errnop) { + _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; + _cleanup_free_ char **members = NULL; enum nss_status ret; - UserEntry *p; - size_t len; + int r; PROTECT_ERRNO; BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); assert(result); - assert(buffer); assert(errnop); - assert_se(pthread_mutex_lock(&getpwent_data.mutex) == 0); + r = userdb_nss_compat_is_enabled(); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + if (!r) { + UNPROTECT_ERRNO; + *errnop = EHOSTDOWN; + return NSS_STATUS_UNAVAIL; + } + + assert_se(pthread_mutex_lock(&getgrent_data.mutex) == 0); - LIST_FOREACH(entries, p, getpwent_data.position) { - len = 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; + } } + } - 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 (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; + } } - if (!p) { - ret = NSS_STATUS_NOTFOUND; - goto finalize; + + r = nss_pack_group_record(gr, members, result, buffer, buflen); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + ret = NSS_STATUS_TRYAGAIN; + goto finish; } - /* 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; - - assert_se(pthread_mutex_unlock(&getpwent_data.mutex) == 0); - +finish: + assert_se(pthread_mutex_unlock(&getgrent_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 ret; - UserEntry *p; - size_t len; +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(result); - assert(buffer); + assert(user_name); + assert(start); + assert(size); + assert(groupsp); assert(errnop); - assert_se(pthread_mutex_lock(&getgrent_data.mutex) == 0); + 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) { + UNPROTECT_ERRNO; + *errnop = EHOSTDOWN; + return NSS_STATUS_UNAVAIL; + } + + r = membershipdb_by_user(user_name, nss_glue_userdb_flags(), &iterator); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } - LIST_FOREACH(entries, p, getgrent_data.position) { - len = sizeof(char*) + strlen(p->name) + 1; - if (buflen < len) { + 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 = ERANGE; - ret = NSS_STATUS_TRYAGAIN; - goto finalize; + *errnop = -r; + return NSS_STATUS_UNAVAIL; } - memzero(buffer, sizeof(char*)); - strcpy(buffer + sizeof(char*), p->name); + /* 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. */ - 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; - } + 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; + } - /* On success, step to the next entry. */ - p = p->entries_next; - ret = NSS_STATUS_SUCCESS; + if (g->gid == gid) + continue; -finalize: - /* Save position for the next call. */ - getgrent_data.position = p; + 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; + } - assert_se(pthread_mutex_unlock(&getgrent_data.mutex) == 0); + (*groupsp)[(*start)++] = g->gid; + any = true; + } - return ret; + 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 ff63382b152c9..77e1fbe93f227 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 0000000000000..810ac56286d18 --- /dev/null +++ b/src/nss-systemd/userdb-glue.c @@ -0,0 +1,352 @@ +/* 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) { + *errnop = EHOSTDOWN; + return NSS_STATUS_UNAVAIL; + } + + 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) { + *errnop = EHOSTDOWN; + return NSS_STATUS_UNAVAIL; + } + + 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) { + *errnop = EHOSTDOWN; + return NSS_STATUS_UNAVAIL; + } + + 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) { + *errnop = EHOSTDOWN; + return NSS_STATUS_UNAVAIL; + } + + 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 0000000000000..02add24b6b814 --- /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/portable/portable.c b/src/portable/portable.c index d37880cfd1d8f..15a4c2c618bde 100644 --- a/src/portable/portable.c +++ b/src/portable/portable.c @@ -1,5 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ +#include + #include "bus-common-errors.h" #include "bus-error.h" #include "conf-files.h" @@ -359,7 +361,7 @@ static int portable_extract_by_path( assert(path); - r = loop_device_make_by_path(path, O_RDONLY, &d); + r = loop_device_make_by_path(path, O_RDONLY, LO_FLAGS_PARTSCAN, &d); if (r == -EISDIR) { /* We can't turn this into a loop-back block device, and this returns EISDIR? Then this is a directory * tree and not a raw device. It's easy then. */ diff --git a/src/core/chown-recursive.c b/src/shared/chown-recursive.c similarity index 76% rename from src/core/chown-recursive.c rename to src/shared/chown-recursive.c index 24cdf25b838b2..1aebac307ac30 100644 --- a/src/core/chown-recursive.c +++ b/src/shared/chown-recursive.c @@ -139,3 +139,40 @@ int path_chown_recursive( return chown_recursive_internal(TAKE_FD(fd), &st, uid, gid, mask); /* we donate the fd to the call, regardless if it succeeded or failed */ } + +int fd_chown_recursive( + int fd, + uid_t uid, + gid_t gid, + mode_t mask) { + + int duplicated_fd = -1; + struct stat st; + + /* Note that the slightly different order of fstat() and the checks here and in + * path_chown_recursive(). That's because when we open the dirctory ourselves we can specify + * O_DIRECTORY and we always want to ensure we are operating on a directory before deciding whether + * the operation is otherwise redundant. */ + + if (fstat(fd, &st) < 0) + return -errno; + + if (!S_ISDIR(st.st_mode)) + return -ENOTDIR; + + if (!uid_is_valid(uid) && !gid_is_valid(gid) && (mask & 07777) == 07777) + return 0; /* nothing to do */ + + /* Shortcut, as above */ + if ((!uid_is_valid(uid) || st.st_uid == uid) && + (!gid_is_valid(gid) || st.st_gid == gid) && + ((st.st_mode & ~mask & 07777) == 0)) + return 0; + + /* Let's duplicate the fd here, as opendir() wants to take possession of it and close it afterwards */ + duplicated_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3); + if (duplicated_fd < 0) + return -errno; + + return chown_recursive_internal(duplicated_fd, &st, uid, gid, mask); /* fd donated even on failure */ +} diff --git a/src/core/chown-recursive.h b/src/shared/chown-recursive.h similarity index 69% rename from src/core/chown-recursive.h rename to src/shared/chown-recursive.h index bfee05f3be561..14a79733f531e 100644 --- a/src/core/chown-recursive.h +++ b/src/shared/chown-recursive.h @@ -4,3 +4,5 @@ #include int path_chown_recursive(const char *path, uid_t uid, gid_t gid, mode_t mask); + +int fd_chown_recursive(int fd, uid_t uid, gid_t gid, mode_t mask); diff --git a/src/shared/format-table.h b/src/shared/format-table.h index aacf978978847..80f319d054a30 100644 --- a/src/shared/format-table.h +++ b/src/shared/format-table.h @@ -53,6 +53,12 @@ typedef enum TableDataType { #define TABLE_PID TABLE_INT32 assert_cc(sizeof(pid_t) == sizeof(int32_t)); +/* UIDs/GIDs are just 32bit unsigned integers on Linux */ +#define TABLE_UID TABLE_UINT32 +#define TABLE_GID TABLE_UINT32 +assert_cc(sizeof(uid_t) == sizeof(uint32_t)); +assert_cc(sizeof(gid_t) == sizeof(uint32_t)); + typedef struct Table Table; typedef struct TableCell TableCell; diff --git a/src/shared/gpt.h b/src/shared/gpt.h index 31e01bd5a5cc9..3d44514d12407 100644 --- a/src/shared/gpt.h +++ b/src/shared/gpt.h @@ -19,6 +19,7 @@ #define GPT_SWAP SD_ID128_MAKE(06,57,fd,6d,a4,ab,43,c4,84,e5,09,33,c8,4b,4f,4f) #define GPT_HOME SD_ID128_MAKE(93,3a,c7,e1,2e,b4,4f,13,b8,44,0e,14,e2,ae,f9,15) #define GPT_SRV SD_ID128_MAKE(3b,8f,84,25,20,e0,4f,3b,90,7f,1a,25,a7,6f,98,e8) +#define GPT_USER_HOME SD_ID128_MAKE(77,3f,91,ef,66,d4,49,b5,bd,83,d6,83,bf,40,ad,16) /* Verity partitions for the root partitions above (we only define them for the root partitions, because only they are * are commonly read-only and hence suitable for verity). */ diff --git a/src/shared/group-record-nss.c b/src/shared/group-record-nss.c new file mode 100644 index 0000000000000..e31f2ce697ac5 --- /dev/null +++ b/src/shared/group-record-nss.c @@ -0,0 +1,211 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "errno-util.h" +#include "group-record-nss.h" +#include "strv.h" +#include "user-util.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; + + r = free_and_strdup(&g->group_name, grp->gr_name); + if (r < 0) + return r; + + strv_free(g->members); + 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)) { + strv_free_erase(g->hashed_password); + g->hashed_password = strv_new(sgrp->sg_passwd); + if (!g->hashed_password) + return -ENOMEM; + } else + g->hashed_password = strv_free_erase(g->hashed_password); + + r = strv_extend_strv(&g->members, sgrp->sg_mem, 1); + if (r < 0) + return r; + + strv_free(g->administrators); + g->administrators = strv_copy(sgrp->sg_adm); + if (!g->administrators) + return -ENOMEM; + } else { + g->hashed_password = strv_free_erase(g->hashed_password); + g->administrators = strv_free(g->administrators); + } + + g->json = json_variant_unref(g->json); + 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 0000000000000..38b2995178ff7 --- /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 0000000000000..d0300e483c743 --- /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 0000000000000..12bdbd17243c9 --- /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 0000000000000..5c4534242f5c7 --- /dev/null +++ b/src/shared/group-record.c @@ -0,0 +1,347 @@ +/* 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 (!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 (!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 (!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); + JsonVariant *per_machine, *binding, *status; + 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)) != 0) + 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' and 'binding' fields, since we want + * them to override the global options. Let's process them now. */ + + per_machine = json_variant_by_key(h->json, "perMachine"); + if (per_machine) { + r = dispatch_per_machine("perMachine", per_machine, json_flags, h); + if (r < 0) + return r; + } + + binding = json_variant_by_key(h->json, "binding"); + if (binding) { + r = dispatch_binding("binding", binding, json_flags, h); + if (r < 0) + return r; + } + + status = json_variant_by_key(h->json, "status"); + if (status) { + r = dispatch_status("binding", 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 0000000000000..b72a43e50d8d6 --- /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/json.c b/src/shared/json.c index f1bb50cfa2c8c..6222c188a1843 100644 --- a/src/shared/json.c +++ b/src/shared/json.c @@ -24,6 +24,7 @@ #include "string-util.h" #include "strv.h" #include "terminal-util.h" +#include "user-util.h" #include "utf8.h" /* Refuse putting together variants with a larger depth than 4K by default (as a protection against overflowing stacks @@ -72,6 +73,15 @@ struct JsonVariant { /* While comparing two arrays, we use this for marking what we already have seen */ bool is_marked:1; + /* Erase from memory when freeing */ + bool sensitive:1; + + /* If this is an object the fields are strictly ordered by name */ + bool sorted:1; + + /* If in addition to this object all objects referenced by it are also ordered strictly by name */ + bool normalized:1; + /* The current 'depth' of the JsonVariant, i.e. how many levels of member variants this has */ uint16_t depth; @@ -213,10 +223,10 @@ static uint16_t json_variant_depth(JsonVariant *v) { return v->depth; } -static JsonVariant *json_variant_normalize(JsonVariant *v) { +static JsonVariant *json_variant_formalize(JsonVariant *v) { - /* Converts json variants to their normalized form, i.e. fully dereferenced and wherever possible converted to - * the "magic" version if there is one */ + /* Converts json variant pointers to their normalized form, i.e. fully dereferenced and wherever + * possible converted to the "magic" version if there is one */ if (!v) return NULL; @@ -257,9 +267,9 @@ static JsonVariant *json_variant_normalize(JsonVariant *v) { } } -static JsonVariant *json_variant_conservative_normalize(JsonVariant *v) { +static JsonVariant *json_variant_conservative_formalize(JsonVariant *v) { - /* Much like json_variant_normalize(), but won't simplify if the variant has a source/line location attached to + /* Much like json_variant_formalize(), but won't simplify if the variant has a source/line location attached to * it, in order not to lose context */ if (!v) @@ -271,7 +281,7 @@ static JsonVariant *json_variant_conservative_normalize(JsonVariant *v) { if (v->source || v->line > 0 || v->column > 0) return v; - return json_variant_normalize(v); + return json_variant_formalize(v); } static int json_variant_new(JsonVariant **ret, JsonVariantType type, size_t space) { @@ -403,6 +413,20 @@ int json_variant_new_stringn(JsonVariant **ret, const char *s, size_t n) { return 0; } +int json_variant_new_base64(JsonVariant **ret, const void *p, size_t n) { + _cleanup_free_ char *s = NULL; + ssize_t k; + + assert_return(ret, -EINVAL); + assert_return(n == 0 || p, -EINVAL); + + k = base64mem(p, n, &s); + if (k < 0) + return k; + + return json_variant_new_stringn(ret, s, k); +} + static void json_variant_set(JsonVariant *a, JsonVariant *b) { assert(a); @@ -449,7 +473,7 @@ static void json_variant_set(JsonVariant *a, JsonVariant *b) { case JSON_VARIANT_ARRAY: case JSON_VARIANT_OBJECT: a->is_reference = true; - a->reference = json_variant_ref(json_variant_conservative_normalize(b)); + a->reference = json_variant_ref(json_variant_conservative_formalize(b)); break; case JSON_VARIANT_NULL: @@ -474,6 +498,7 @@ static void json_variant_copy_source(JsonVariant *v, JsonVariant *from) { int json_variant_new_array(JsonVariant **ret, JsonVariant **array, size_t n) { _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + bool normalized = true; assert_return(ret, -EINVAL); if (n == 0) { @@ -509,8 +534,13 @@ int json_variant_new_array(JsonVariant **ret, JsonVariant **array, size_t n) { json_variant_set(w, c); json_variant_copy_source(w, c); + + if (!json_variant_is_normalized(c)) + normalized = false; } + v->normalized = normalized; + *ret = TAKE_PTR(v); return 0; } @@ -548,6 +578,8 @@ int json_variant_new_array_bytes(JsonVariant **ret, const void *p, size_t n) { }; } + v->normalized = true; + *ret = v; return 0; } @@ -599,12 +631,16 @@ int json_variant_new_array_strv(JsonVariant **ret, char **l) { memcpy(w->string, l[v->n_elements], k+1); } + v->normalized = true; + *ret = TAKE_PTR(v); return 0; } int json_variant_new_object(JsonVariant **ret, JsonVariant **array, size_t n) { _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + const char *prev = NULL; + bool sorted = true, normalized = true; assert_return(ret, -EINVAL); if (n == 0) { @@ -628,9 +664,20 @@ int json_variant_new_object(JsonVariant **ret, JsonVariant **array, size_t n) { *c = array[v->n_elements]; uint16_t d; - if ((v->n_elements & 1) == 0 && - !json_variant_is_string(c)) - return -EINVAL; /* Every second one needs to be a string, as it is the key name */ + if ((v->n_elements & 1) == 0) { + const char *k; + + if (!json_variant_is_string(c)) + return -EINVAL; /* Every second one needs to be a string, as it is the key name */ + + assert_se(k = json_variant_string(c)); + + if (prev && strcmp(k, prev) <= 0) + sorted = normalized = false; + + prev = k; + } else if (!json_variant_is_normalized(c)) + normalized = false; d = json_variant_depth(c); if (d >= DEPTH_MAX) /* Refuse too deep nesting */ @@ -647,11 +694,53 @@ int json_variant_new_object(JsonVariant **ret, JsonVariant **array, size_t n) { json_variant_copy_source(w, c); } + v->normalized = normalized; + v->sorted = sorted; + *ret = TAKE_PTR(v); return 0; } -static void json_variant_free_inner(JsonVariant *v) { +static size_t json_variant_size(JsonVariant* v) { + + if (!json_variant_is_regular(v)) + return 0; + + if (v->is_reference) + return offsetof(JsonVariant, reference) + sizeof(JsonVariant*); + + switch (v->type) { + + case JSON_VARIANT_STRING: + return offsetof(JsonVariant, string) + strlen(v->string) + 1; + + case JSON_VARIANT_REAL: + return offsetof(JsonVariant, value) + sizeof(long double); + + case JSON_VARIANT_UNSIGNED: + return offsetof(JsonVariant, value) + sizeof(uintmax_t); + + case JSON_VARIANT_INTEGER: + return offsetof(JsonVariant, value) + sizeof(intmax_t); + + case JSON_VARIANT_BOOLEAN: + return offsetof(JsonVariant, value) + sizeof(bool); + + case JSON_VARIANT_ARRAY: + case JSON_VARIANT_OBJECT: + return offsetof(JsonVariant, n_elements) + sizeof(size_t); + + case JSON_VARIANT_NULL: + return offsetof(JsonVariant, value); + + default: + assert_not_reached("unexpected type"); + } +} + +static void json_variant_free_inner(JsonVariant *v, bool force_sensitive) { + bool sensitive; + assert(v); if (!json_variant_is_regular(v)) @@ -659,7 +748,12 @@ static void json_variant_free_inner(JsonVariant *v) { json_source_unref(v->source); + sensitive = v->sensitive || force_sensitive; + if (v->is_reference) { + if (sensitive) + json_variant_sensitive(v->reference); + json_variant_unref(v->reference); return; } @@ -668,8 +762,11 @@ static void json_variant_free_inner(JsonVariant *v) { size_t i; for (i = 0; i < v->n_elements; i++) - json_variant_free_inner(v + 1 + i); + json_variant_free_inner(v + 1 + i, sensitive); } + + if (sensitive) + explicit_bzero(v, json_variant_size(v)); } JsonVariant *json_variant_ref(JsonVariant *v) { @@ -701,7 +798,7 @@ JsonVariant *json_variant_unref(JsonVariant *v) { v->n_ref--; if (v->n_ref == 0) { - json_variant_free_inner(v); + json_variant_free_inner(v, false); free(v); } } @@ -947,6 +1044,13 @@ bool json_variant_is_negative(JsonVariant *v) { return false; } +bool json_variant_is_blank_object(JsonVariant *v) { + /* Returns true if the specified object is null or empty */ + return !v || + json_variant_is_null(v) || + (json_variant_is_object(v) && json_variant_elements(v) == 0); +} + JsonVariantType json_variant_type(JsonVariant *v) { if (!v) @@ -1069,7 +1173,7 @@ JsonVariant *json_variant_by_index(JsonVariant *v, size_t idx) { if (idx >= v->n_elements) return NULL; - return json_variant_conservative_normalize(v + 1 + idx); + return json_variant_conservative_formalize(v + 1 + idx); mismatch: log_debug("Element in non-array/non-object JSON variant requested by index, returning NULL."); @@ -1092,6 +1196,37 @@ JsonVariant *json_variant_by_key_full(JsonVariant *v, const char *key, JsonVaria if (v->is_reference) return json_variant_by_key(v->reference, key); + if (v->sorted) { + size_t a = 0, b = v->n_elements/2; + + /* If the variant is sorted we can use bisection to find the entry we need in O(log(n)) time */ + + while (b > a) { + JsonVariant *p; + const char *f; + int c; + + i = (a + b) / 2; + p = json_variant_dereference(v + 1 + i*2); + + assert_se(f = json_variant_string(p)); + + c = strcmp(key, f); + if (c == 0) { + if (ret_key) + *ret_key = json_variant_conservative_formalize(v + 1 + i*2); + + return json_variant_conservative_formalize(v + 1 + i*2 + 1); + } else if (c < 0) + b = i; + else + a = i + 1; + } + + goto not_found; + } + + /* The variant is not sorted, hence search for the field linearly */ for (i = 0; i < v->n_elements; i += 2) { JsonVariant *p; @@ -1103,9 +1238,9 @@ JsonVariant *json_variant_by_key_full(JsonVariant *v, const char *key, JsonVaria if (streq(json_variant_string(p), key)) { if (ret_key) - *ret_key = json_variant_conservative_normalize(v + 1 + i); + *ret_key = json_variant_conservative_formalize(v + 1 + i); - return json_variant_conservative_normalize(v + 1 + i + 1); + return json_variant_conservative_formalize(v + 1 + i + 1); } } @@ -1130,8 +1265,8 @@ JsonVariant *json_variant_by_key(JsonVariant *v, const char *key) { bool json_variant_equal(JsonVariant *a, JsonVariant *b) { JsonVariantType t; - a = json_variant_normalize(a); - b = json_variant_normalize(b); + a = json_variant_formalize(a); + b = json_variant_formalize(b); if (a == b) return true; @@ -1230,6 +1365,26 @@ bool json_variant_equal(JsonVariant *a, JsonVariant *b) { } } +void json_variant_sensitive(JsonVariant *v) { + assert(v); + + /* Marks a variant as "sensitive", so that it is erased from memory when it is destroyed. This is a + * one-way operation: as soon as it is marked this way it remains marked this way until it's + * destoryed. A magic variant is never sensitive though, even when asked, since it's too + * basic. Similar, const string variant are never sensitive either, after all they are included in + * the source code as they are, which is not suitable for inclusion of secrets. + * + * Note that this flag has a recursive effect: when we destroy an object or array we'll propagate the + * flag to all contained variants. And if those are then destroyed this is propagated further down, + * and so on. */ + + v = json_variant_formalize(v); + if (!json_variant_is_regular(v)) + return; + + v->sensitive = true; +} + int json_variant_get_source(JsonVariant *v, const char **ret_source, unsigned *ret_line, unsigned *ret_column) { assert_return(v, -EINVAL); @@ -1597,6 +1752,9 @@ void json_variant_dump(JsonVariant *v, JsonFormatFlags flags, FILE *f, const cha if (((flags & (JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_COLOR)) == JSON_FORMAT_COLOR_AUTO) && colors_enabled()) flags |= JSON_FORMAT_COLOR; + if (((flags & (JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_PRETTY)) == JSON_FORMAT_PRETTY_AUTO)) + flags |= on_tty() ? JSON_FORMAT_PRETTY : JSON_FORMAT_NEWLINE; + if (flags & JSON_FORMAT_SSE) fputs("data: ", f); if (flags & JSON_FORMAT_SEQ) @@ -1608,6 +1766,341 @@ void json_variant_dump(JsonVariant *v, JsonFormatFlags flags, FILE *f, const cha fputc('\n', f); if (flags & JSON_FORMAT_SSE) fputc('\n', f); /* In case of SSE add a second newline */ + + if (flags & JSON_FORMAT_FLUSH) + fflush(f); +} + +int json_variant_filter(JsonVariant **v, char **to_remove) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + _cleanup_free_ JsonVariant **array = NULL; + size_t i, n = 0, k = 0; + int r; + + assert(v); + + if (json_variant_is_blank_object(*v)) + return 0; + if (!json_variant_is_object(*v)) + return -EINVAL; + + if (strv_isempty(to_remove)) + return 0; + + for (i = 0; i < json_variant_elements(*v); i += 2) { + JsonVariant *p; + + p = json_variant_by_index(*v, i); + if (!json_variant_has_type(p, JSON_VARIANT_STRING)) + return -EINVAL; + + if (strv_contains(to_remove, json_variant_string(p))) { + if (!array) { + array = new(JsonVariant*, json_variant_elements(*v) - 2); + if (!array) + return -ENOMEM; + + for (k = 0; k < i; k++) + array[k] = json_variant_by_index(*v, k); + } + + n++; + } else if (array) { + array[k++] = p; + array[k++] = json_variant_by_index(*v, i + 1); + } + } + + if (n == 0) + return 0; + + r = json_variant_new_object(&w, array, k); + if (r < 0) + return r; + + json_variant_unref(*v); + *v = TAKE_PTR(w); + + return (int) n; +} + +int json_variant_set_field(JsonVariant **v, const char *field, JsonVariant *value) { + _cleanup_(json_variant_unrefp) JsonVariant *field_variant = NULL, *w = NULL; + _cleanup_free_ JsonVariant **array = NULL; + size_t i, k = 0; + int r; + + assert(v); + assert(field); + + if (json_variant_is_blank_object(*v)) { + array = new(JsonVariant*, 2); + if (!array) + return -ENOMEM; + + } else { + if (!json_variant_is_object(*v)) + return -EINVAL; + + for (i = 0; i < json_variant_elements(*v); i += 2) { + JsonVariant *p; + + p = json_variant_by_index(*v, i); + if (!json_variant_is_string(p)) + return -EINVAL; + + if (streq(json_variant_string(p), field)) { + + if (!array) { + array = new(JsonVariant*, json_variant_elements(*v)); + if (!array) + return -ENOMEM; + + for (k = 0; k < i; k++) + array[k] = json_variant_by_index(*v, k); + } + + } else if (array) { + array[k++] = p; + array[k++] = json_variant_by_index(*v, i + 1); + } + } + + if (!array) { + array = new(JsonVariant*, json_variant_elements(*v) + 2); + if (!array) + return -ENOMEM; + + for (k = 0; k < json_variant_elements(*v); k++) + array[k] = json_variant_by_index(*v, k); + } + } + + r = json_variant_new_string(&field_variant, field); + if (r < 0) + return r; + + array[k++] = field_variant; + array[k++] = value; + + r = json_variant_new_object(&w, array, k); + if (r < 0) + return r; + + json_variant_unref(*v); + *v = TAKE_PTR(w); + + return 1; +} + +int json_variant_set_field_string(JsonVariant **v, const char *field, const char *value) { + _cleanup_(json_variant_unrefp) JsonVariant *m = NULL; + int r; + + r = json_variant_new_string(&m, value); + if (r < 0) + return r; + + return json_variant_set_field(v, field, m); +} + +int json_variant_set_field_integer(JsonVariant **v, const char *field, intmax_t i) { + _cleanup_(json_variant_unrefp) JsonVariant *m = NULL; + int r; + + r = json_variant_new_integer(&m, i); + if (r < 0) + return r; + + return json_variant_set_field(v, field, m); +} + +int json_variant_set_field_unsigned(JsonVariant **v, const char *field, uintmax_t u) { + _cleanup_(json_variant_unrefp) JsonVariant *m = NULL; + int r; + + r = json_variant_new_unsigned(&m, u); + if (r < 0) + return r; + + return json_variant_set_field(v, field, m); +} + +int json_variant_set_field_boolean(JsonVariant **v, const char *field, bool b) { + _cleanup_(json_variant_unrefp) JsonVariant *m = NULL; + int r; + + r = json_variant_new_boolean(&m, b); + if (r < 0) + return r; + + return json_variant_set_field(v, field, m); +} + +int json_variant_merge(JsonVariant **v, JsonVariant *m) { + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + _cleanup_free_ JsonVariant **array = NULL; + size_t v_elements, m_elements, i, k; + bool v_blank, m_blank; + int r; + + m = json_variant_dereference(m); + + v_blank = json_variant_is_blank_object(*v); + m_blank = json_variant_is_blank_object(m); + + if (!v_blank && !json_variant_is_object(*v)) + return -EINVAL; + if (!m_blank && !json_variant_is_object(m)) + return -EINVAL; + + if (m_blank) + return 0; /* nothing to do */ + + if (v_blank) { + json_variant_unref(*v); + *v = json_variant_ref(m); + return 1; + } + + v_elements = json_variant_elements(*v); + m_elements = json_variant_elements(m); + if (v_elements > SIZE_MAX - m_elements) /* overflow check */ + return -ENOMEM; + + array = new(JsonVariant*, v_elements + m_elements); + if (!array) + return -ENOMEM; + + k = 0; + for (i = 0; i < v_elements; i += 2) { + JsonVariant *u; + + u = json_variant_by_index(*v, i); + if (!json_variant_is_string(u)) + return -EINVAL; + + if (json_variant_by_key(m, json_variant_string(u))) + continue; /* skip if exists in second variant */ + + array[k++] = u; + array[k++] = json_variant_by_index(*v, i + 1); + } + + for (i = 0; i < m_elements; i++) + array[k++] = json_variant_by_index(m, i); + + r = json_variant_new_object(&w, array, k); + if (r < 0) + return r; + + json_variant_unref(*v); + *v = TAKE_PTR(w); + + return 1; +} + +int json_variant_append_array(JsonVariant **v, JsonVariant *element) { + _cleanup_(json_variant_unrefp) JsonVariant *nv = NULL; + bool blank; + int r; + + assert(v); + assert(element); + + + if (!*v || json_variant_is_null(*v)) + blank = true; + else if (!json_variant_is_array(*v)) + return -EINVAL; + else + blank = json_variant_elements(*v) == 0; + + if (blank) + r = json_variant_new_array(&nv, (JsonVariant*[]) { element }, 1); + else { + _cleanup_free_ JsonVariant **array = NULL; + size_t i; + + array = new(JsonVariant*, json_variant_elements(*v)); + if (array) + return -ENOMEM; + + for (i = 0; i < json_variant_elements(*v); i++) + array[i] = json_variant_by_index(*v, i); + + array[i] = element; + + r = json_variant_new_array(&nv, array, i + 1); + } + + if (r < 0) + return r; + + json_variant_unref(*v); + *v = TAKE_PTR(nv); + + return 0; +} + +int json_variant_strv(JsonVariant *v, char ***ret) { + char **l = NULL; + size_t n, i; + bool sensitive; + int r; + + assert(ret); + + if (!v || json_variant_is_null(v)) { + l = new0(char*, 1); + if (!l) + return -ENOMEM; + + *ret = l; + return 0; + } + + if (!json_variant_is_array(v)) + return -EINVAL; + + sensitive = v->sensitive; + + n = json_variant_elements(v); + l = new(char*, n+1); + if (!l) + return -ENOMEM; + + for (i = 0; i < n; i++) { + JsonVariant *e; + + assert_se(e = json_variant_by_index(v, i)); + sensitive = sensitive || e->sensitive; + + if (!json_variant_is_string(e)) { + l[i] = NULL; + r = -EINVAL; + goto fail; + } + + l[i] = strdup(json_variant_string(e)); + if (!l[i]) { + r = -ENOMEM; + goto fail; + } + } + + l[i] = NULL; + *ret = TAKE_PTR(l); + + return 0; + +fail: + if (sensitive) + strv_free_erase(l); + else + strv_free(l); + + return r; } static int json_variant_copy(JsonVariant **nv, JsonVariant *v) { @@ -1673,7 +2166,7 @@ static int json_variant_copy(JsonVariant **nv, JsonVariant *v) { c->n_ref = 1; c->type = t; c->is_reference = true; - c->reference = json_variant_ref(json_variant_normalize(v)); + c->reference = json_variant_ref(json_variant_formalize(v)); *nv = c; return 0; @@ -2275,6 +2768,7 @@ static void json_stack_release(JsonStack *s) { static int json_parse_internal( const char **input, JsonSource *source, + JsonParseFlags flags, JsonVariant **ret, unsigned *line, unsigned *column, @@ -2593,6 +3087,12 @@ static int json_parse_internal( } if (add) { + /* If we are asked to make this parsed object sensitive, then let's apply this + * immediately after allocating each variant, so that when we abort half-way + * everything we already allocated that is then freed is correctly marked. */ + if (FLAGS_SET(flags, JSON_PARSE_SENSITIVE)) + json_variant_sensitive(add); + (void) json_variant_set_source(&add, source, line_token, column_token); if (!GREEDY_REALLOC(current->elements, current->n_elements_allocated, current->n_elements + 1)) { @@ -2621,15 +3121,15 @@ static int json_parse_internal( return r; } -int json_parse(const char *input, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { - return json_parse_internal(&input, NULL, ret, ret_line, ret_column, false); +int json_parse(const char *input, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { + return json_parse_internal(&input, NULL, flags, ret, ret_line, ret_column, false); } -int json_parse_continue(const char **p, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { - return json_parse_internal(p, NULL, ret, ret_line, ret_column, true); +int json_parse_continue(const char **p, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { + return json_parse_internal(p, NULL, flags, ret, ret_line, ret_column, true); } -int json_parse_file(FILE *f, const char *path, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { +int json_parse_file_at(FILE *f, int dir_fd, const char *path, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { _cleanup_(json_source_unrefp) JsonSource *source = NULL; _cleanup_free_ char *text = NULL; const char *p; @@ -2638,7 +3138,7 @@ int json_parse_file(FILE *f, const char *path, JsonVariant **ret, unsigned *ret_ if (f) r = read_full_stream(f, &text, NULL); else if (path) - r = read_full_file(path, &text, NULL); + r = read_full_file_full(dir_fd, path, 0, &text, NULL); else return -EINVAL; if (r < 0) @@ -2651,7 +3151,7 @@ int json_parse_file(FILE *f, const char *path, JsonVariant **ret, unsigned *ret_ } p = text; - return json_parse_internal(&p, source, ret, ret_line, ret_column, false); + return json_parse_internal(&p, source, flags, ret, ret_line, ret_column, false); } int json_buildv(JsonVariant **ret, va_list ap) { @@ -2875,6 +3375,36 @@ int json_buildv(JsonVariant **ret, va_list ap) { break; + case _JSON_BUILD_VARIANT_ARRAY: { + JsonVariant **array; + size_t n; + + if (!IN_SET(current->expect, EXPECT_TOPLEVEL, EXPECT_OBJECT_VALUE, EXPECT_ARRAY_ELEMENT)) { + r = -EINVAL; + goto finish; + } + + array = va_arg(ap, JsonVariant**); + n = va_arg(ap, size_t); + + if (current->n_suppress == 0) { + r = json_variant_new_array(&add, array, n); + if (r < 0) + goto finish; + } + + n_subtract = 1; + + if (current->expect == EXPECT_TOPLEVEL) + current->expect = EXPECT_END; + else if (current->expect == EXPECT_OBJECT_VALUE) + current->expect = EXPECT_OBJECT_KEY; + else + assert(current->expect == EXPECT_ARRAY_ELEMENT); + + break; + } + case _JSON_BUILD_LITERAL: { const char *l; @@ -2889,7 +3419,7 @@ int json_buildv(JsonVariant **ret, va_list ap) { /* Note that we don't care for current->n_suppress here, we should generate parsing * errors even in suppressed object properties */ - r = json_parse(l, &add, NULL, NULL); + r = json_parse(l, 0, &add, NULL, NULL); if (r < 0) goto finish; } else @@ -2986,6 +3516,36 @@ int json_buildv(JsonVariant **ret, va_list ap) { break; } + case _JSON_BUILD_BASE64: { + const void *p; + size_t n; + + if (!IN_SET(current->expect, EXPECT_TOPLEVEL, EXPECT_OBJECT_VALUE, EXPECT_ARRAY_ELEMENT)) { + r = -EINVAL; + goto finish; + } + + p = va_arg(ap, const void *); + n = va_arg(ap, size_t); + + if (current->n_suppress == 0) { + r = json_variant_new_base64(&add, p, n); + if (r < 0) + goto finish; + } + + n_subtract = 1; + + if (current->expect == EXPECT_TOPLEVEL) + current->expect = EXPECT_END; + else if (current->expect == EXPECT_OBJECT_VALUE) + current->expect = EXPECT_OBJECT_KEY; + else + assert(current->expect == EXPECT_ARRAY_ELEMENT); + + break; + } + case _JSON_BUILD_OBJECT_BEGIN: if (!IN_SET(current->expect, EXPECT_TOPLEVEL, EXPECT_OBJECT_VALUE, EXPECT_ARRAY_ELEMENT)) { @@ -3320,6 +3880,11 @@ int json_dispatch_tristate(const char *name, JsonVariant *variant, JsonDispatchF assert(variant); assert(b); + if (json_variant_is_null(variant)) { + *b = -1; + return 0; + } + if (!json_variant_is_boolean(variant)) return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a boolean.", strna(name)); @@ -3400,6 +3965,9 @@ int json_dispatch_string(const char *name, JsonVariant *variant, JsonDispatchFla if (!json_variant_is_string(variant)) return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + if ((flags & JSON_SAFE) && !string_is_safe(json_variant_string(variant))) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' contains unsafe characters, refusing.", strna(name)); + r = free_and_strdup(s, json_variant_string(variant)); if (r < 0) return json_log(variant, flags, r, "Failed to allocate string: %m"); @@ -3407,6 +3975,27 @@ int json_dispatch_string(const char *name, JsonVariant *variant, JsonDispatchFla return 0; } +int json_dispatch_const_string(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + const char **s = userdata; + + assert(variant); + assert(s); + + if (json_variant_is_null(variant)) { + *s = NULL; + 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)); + + if ((flags & JSON_SAFE) && !string_is_safe(json_variant_string(variant))) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' contains unsafe characters, refusing.", strna(name)); + + *s = json_variant_string(variant); + return 0; +} + int json_dispatch_strv(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { _cleanup_strv_free_ char **l = NULL; char ***s = userdata; @@ -3421,6 +4010,19 @@ int json_dispatch_strv(const char *name, JsonVariant *variant, JsonDispatchFlags return 0; } + /* Let's be flexible here: accept a single string in place of a single-item array */ + if (json_variant_is_string(variant)) { + if ((flags & JSON_SAFE) && !string_is_safe(json_variant_string(variant))) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' contains unsafe characters, refusing.", strna(name)); + + l = strv_new(json_variant_string(variant)); + if (!l) + return log_oom(); + + strv_free_and_replace(*s, l); + return 0; + } + if (!json_variant_is_array(variant)) return json_log(variant, SYNTHETIC_ERRNO(EINVAL), flags, "JSON field '%s' is not an array.", strna(name)); @@ -3428,6 +4030,9 @@ int json_dispatch_strv(const char *name, JsonVariant *variant, JsonDispatchFlags if (!json_variant_is_string(e)) return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not a string."); + if ((flags & JSON_SAFE) && !string_is_safe(json_variant_string(e))) + return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' contains unsafe characters, refusing.", strna(name)); + r = strv_extend(&l, json_variant_string(e)); if (r < 0) return json_log(e, flags, r, "Failed to append array element: %m"); @@ -3449,6 +4054,227 @@ int json_dispatch_variant(const char *name, JsonVariant *variant, JsonDispatchFl return 0; } +int json_dispatch_uid_gid(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + uid_t *uid = userdata; + uintmax_t k; + + assert_cc(sizeof(uid_t) == sizeof(uint32_t)); + assert_cc(sizeof(gid_t) == sizeof(uint32_t)); + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtype-limits" + assert_cc(((uid_t) -1 < (uid_t) 0) == ((gid_t) -1 < (gid_t) 0)); +#pragma GCC diagnostic pop + + if (json_variant_is_null(variant)) { + *uid = UID_INVALID; + 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 > UINT32_MAX || !uid_is_valid(k)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid UID/GID.", strna(name)); + + *uid = k; + return 0; +} + +int json_dispatch_user_group_name(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_user_group_name(n)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid user/group 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; +} + +int json_dispatch_id128(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + sd_id128_t *uuid = userdata; + int r; + + if (json_variant_is_null(variant)) { + *uuid = SD_ID128_NULL; + 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 = sd_id128_from_string(json_variant_string(variant), uuid); + if (r < 0) + return json_log(variant, flags, r, "JSON field '%s' is not a valid UID.", strna(name)); + + return 0; +} + +int json_dispatch_unsupported(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not allowed in this object.", strna(name)); +} + +static int json_cmp_strings(const void *x, const void *y) { + JsonVariant *const *a = x, *const *b = y; + + if (!json_variant_is_string(*a) || !json_variant_is_string(*b)) + return CMP(*a, *b); + + return strcmp(json_variant_string(*a), json_variant_string(*b)); +} + +int json_variant_sort(JsonVariant **v) { + _cleanup_free_ JsonVariant **a = NULL; + JsonVariant *n = NULL; + size_t i, m; + int r; + + assert(v); + + if (json_variant_is_sorted(*v)) + return 0; + + if (!json_variant_is_object(*v)) + return -EMEDIUMTYPE; + + /* Sorts they key/value pairs in an object variant */ + + m = json_variant_elements(*v); + a = new(JsonVariant*, m); + if (!a) + return -ENOMEM; + + for (i = 0; i < m; i++) + a[i] = json_variant_by_index(*v, i); + + qsort(a, m/2, sizeof(JsonVariant*)*2, json_cmp_strings); + + r = json_variant_new_object(&n, a, m); + if (r < 0) + return r; + if (!n->sorted) /* Check if this worked. This will fail if there are multiple identical keys used. */ + return -ENOTUNIQ; + + json_variant_unref(*v); + *v = n; + + return 1; +} + +int json_variant_normalize(JsonVariant **v) { + _cleanup_free_ JsonVariant **a = NULL; + JsonVariant *n = NULL; + size_t i, j, m; + int r; + + assert(v); + + if (json_variant_is_normalized(*v)) + return 0; + + if (!json_variant_is_object(*v) && !json_variant_is_array(*v)) + return -EMEDIUMTYPE; + + /* Sorts the key/value pairs in an object variant anywhere down the tree in the specified variant */ + + m = json_variant_elements(*v); + a = new(JsonVariant*, m); + if (!a) + return -ENOMEM; + + for (i = 0; i < m; i++) { + a[i] = json_variant_ref(json_variant_by_index(*v, i)); + + r = json_variant_normalize(a + i); + if (r < 0) + goto finish; + } + + qsort(a, m/2, sizeof(JsonVariant*)*2, json_cmp_strings); + + if (json_variant_is_object(*v)) + r = json_variant_new_object(&n, a, m); + else { + assert(json_variant_is_array(*v)); + r = json_variant_new_array(&n, a, m); + } + if (r < 0) + goto finish; + if (!n->normalized) { /* Let's see if normalization worked. It will fail if there are multiple + * identical keys used in the same object anywhere, or if there are floating + * point numbers used (see below) */ + r = -ENOTUNIQ; + goto finish; + } + + json_variant_unref(*v); + *v = n; + + r = 1; + +finish: + for (j = 0; j < i; j++) + json_variant_unref(a[j]); + + return r; +} + +bool json_variant_is_normalized(JsonVariant *v) { + + /* For now, let's consider anything containing numbers not expressible as integers as + * non-normalized. That's because we cannot sensibly compare them due to accuracy issues, nor even + * store them if they are too large. */ + if (json_variant_is_real(v) && !json_variant_is_integer(v) && !json_variant_is_unsigned(v)) + return false; + + /* The concept only applies to variants that include other variants, i.e. objects and arrays. All + * others are normalized anyway. */ + if (!json_variant_is_object(v) && !json_variant_is_array(v)) + return true; + + /* Empty objects/arrays don't include any other variant, hence are always normalized too */ + if (json_variant_elements(v) == 0) + return true; + + return v->normalized; /* For everything else there's an explicit boolean we maintain */ +} + +bool json_variant_is_sorted(JsonVariant *v) { + + /* Returns true if all key/value pairs of an object are properly sorted. Note that this only applies + * to objects, not arrays. */ + + if (!json_variant_is_object(v)) + return true; + if (json_variant_elements(v) <= 1) + return true; + + return v->sorted; +} + +int json_variant_unbase64(JsonVariant *v, void **ret, size_t *ret_size) { + + if (!json_variant_is_string(v)) + return -EINVAL; + + return unbase64mem(json_variant_string(v), (size_t) -1, ret, ret_size); +} + static const char* const json_variant_type_table[_JSON_VARIANT_TYPE_MAX] = { [JSON_VARIANT_STRING] = "string", [JSON_VARIANT_INTEGER] = "integer", diff --git a/src/shared/json.h b/src/shared/json.h index 1f9c620ebb816..71f7e606b2086 100644 --- a/src/shared/json.h +++ b/src/shared/json.h @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ #pragma once +#include #include #include #include @@ -54,6 +55,7 @@ typedef enum JsonVariantType { } JsonVariantType; int json_variant_new_stringn(JsonVariant **ret, const char *s, size_t n); +int json_variant_new_base64(JsonVariant **ret, const void *p, size_t n); int json_variant_new_integer(JsonVariant **ret, intmax_t i); int json_variant_new_unsigned(JsonVariant **ret, uintmax_t u); int json_variant_new_real(JsonVariant **ret, long double d); @@ -120,6 +122,9 @@ static inline bool json_variant_is_null(JsonVariant *v) { } bool json_variant_is_negative(JsonVariant *v); +bool json_variant_is_blank_object(JsonVariant *v); +bool json_variant_is_normalized(JsonVariant *v); +bool json_variant_is_sorted(JsonVariant *v); size_t json_variant_elements(JsonVariant *v); JsonVariant *json_variant_by_index(JsonVariant *v, size_t index); @@ -128,6 +133,8 @@ JsonVariant *json_variant_by_key_full(JsonVariant *v, const char *key, JsonVaria bool json_variant_equal(JsonVariant *a, JsonVariant *b); +void json_variant_sensitive(JsonVariant *v); + struct json_variant_foreach_state { JsonVariant *variant; size_t idx; @@ -153,21 +160,48 @@ struct json_variant_foreach_state { int json_variant_get_source(JsonVariant *v, const char **ret_source, unsigned *ret_line, unsigned *ret_column); typedef enum JsonFormatFlags { - JSON_FORMAT_NEWLINE = 1 << 0, /* suffix with newline */ - JSON_FORMAT_PRETTY = 1 << 1, /* add internal whitespace to appeal to human readers */ - JSON_FORMAT_COLOR = 1 << 2, /* insert ANSI color sequences */ - JSON_FORMAT_COLOR_AUTO = 1 << 3, /* insert ANSI color sequences if colors_enabled() says so */ - JSON_FORMAT_SOURCE = 1 << 4, /* prefix with source filename/line/column */ - JSON_FORMAT_SSE = 1 << 5, /* prefix/suffix with W3C server-sent events */ - JSON_FORMAT_SEQ = 1 << 6, /* prefix/suffix with RFC 7464 application/json-seq */ + JSON_FORMAT_NEWLINE = 1 << 0, /* suffix with newline */ + JSON_FORMAT_PRETTY = 1 << 1, /* add internal whitespace to appeal to human readers */ + JSON_FORMAT_PRETTY_AUTO = 1 << 2, /* same, but only if connected to a tty (and JSON_FORMAT_NEWLINE otherwise) */ + JSON_FORMAT_COLOR = 1 << 3, /* insert ANSI color sequences */ + JSON_FORMAT_COLOR_AUTO = 1 << 4, /* insert ANSI color sequences if colors_enabled() says so */ + JSON_FORMAT_SOURCE = 1 << 5, /* prefix with source filename/line/column */ + JSON_FORMAT_SSE = 1 << 6, /* prefix/suffix with W3C server-sent events */ + JSON_FORMAT_SEQ = 1 << 7, /* prefix/suffix with RFC 7464 application/json-seq */ + JSON_FORMAT_FLUSH = 1 << 8, /* call fflush() after dumping JSON */ } JsonFormatFlags; int json_variant_format(JsonVariant *v, JsonFormatFlags flags, char **ret); void json_variant_dump(JsonVariant *v, JsonFormatFlags flags, FILE *f, const char *prefix); -int json_parse(const char *string, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); -int json_parse_continue(const char **p, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); -int json_parse_file(FILE *f, const char *path, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); +int json_variant_filter(JsonVariant **v, char **to_remove); + +int json_variant_set_field(JsonVariant **v, const char *field, JsonVariant *value); +int json_variant_set_field_string(JsonVariant **v, const char *field, const char *value); +int json_variant_set_field_integer(JsonVariant **v, const char *field, intmax_t value); +int json_variant_set_field_unsigned(JsonVariant **v, const char *field, uintmax_t value); +int json_variant_set_field_boolean(JsonVariant **v, const char *field, bool b); + +int json_variant_append_array(JsonVariant **v, JsonVariant *element); + +int json_variant_merge(JsonVariant **v, JsonVariant *m); + +int json_variant_strv(JsonVariant *v, char ***ret); + +int json_variant_sort(JsonVariant **v); +int json_variant_normalize(JsonVariant **v); + +typedef enum JsonParseFlags { + JSON_PARSE_SENSITIVE = 1 << 0, /* mark variant as "sensitive", i.e. something containing secret key material or such */ +} JsonParseFlags; + +int json_parse(const char *string, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); +int json_parse_continue(const char **p, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); +int json_parse_file_at(FILE *f, int dir_fd, const char *path, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column); + +static inline int json_parse_file(FILE *f, const char *path, JsonParseFlags flags, JsonVariant **ret, unsigned *ret_line, unsigned *ret_column) { + return json_parse_file_at(f, AT_FDCWD, path, flags, ret, ret_line, ret_column); +} enum { _JSON_BUILD_STRING, @@ -183,8 +217,10 @@ enum { _JSON_BUILD_PAIR_CONDITION, _JSON_BUILD_NULL, _JSON_BUILD_VARIANT, + _JSON_BUILD_VARIANT_ARRAY, _JSON_BUILD_LITERAL, _JSON_BUILD_STRV, + _JSON_BUILD_BASE64, _JSON_BUILD_MAX, }; @@ -194,13 +230,17 @@ enum { #define JSON_BUILD_REAL(d) _JSON_BUILD_REAL, ({ long double _x = d; _x; }) #define JSON_BUILD_BOOLEAN(b) _JSON_BUILD_BOOLEAN, ({ bool _x = b; _x; }) #define JSON_BUILD_ARRAY(...) _JSON_BUILD_ARRAY_BEGIN, __VA_ARGS__, _JSON_BUILD_ARRAY_END +#define JSON_BUILD_EMPTY_ARRAY _JSON_BUILD_ARRAY_BEGIN, _JSON_BUILD_ARRAY_END #define JSON_BUILD_OBJECT(...) _JSON_BUILD_OBJECT_BEGIN, __VA_ARGS__, _JSON_BUILD_OBJECT_END +#define JSON_BUILD_EMPTY_OBJECT _JSON_BUILD_OBJECT_BEGIN, _JSON_BUILD_OBJECT_END #define JSON_BUILD_PAIR(n, ...) _JSON_BUILD_PAIR, ({ const char *_x = n; _x; }), __VA_ARGS__ #define JSON_BUILD_PAIR_CONDITION(c, n, ...) _JSON_BUILD_PAIR_CONDITION, ({ bool _x = c; _x; }), ({ const char *_x = n; _x; }), __VA_ARGS__ #define JSON_BUILD_NULL _JSON_BUILD_NULL #define JSON_BUILD_VARIANT(v) _JSON_BUILD_VARIANT, ({ JsonVariant *_x = v; _x; }) +#define JSON_BUILD_VARIANT_ARRAY(v, n) _JSON_BUILD_VARIANT_ARRAY, ({ JsonVariant **_x = v; _x; }), ({ size_t _y = n; _y; }) #define JSON_BUILD_LITERAL(l) _JSON_BUILD_LITERAL, ({ const char *_x = l; _x; }) #define JSON_BUILD_STRV(l) _JSON_BUILD_STRV, ({ char **_x = l; _x; }) +#define JSON_BUILD_BASE64(p, n) _JSON_BUILD_BASE64, ({ const void *_x = p; _x; }), ({ size_t _y = n; _y; }) int json_build(JsonVariant **ret, ...); int json_buildv(JsonVariant **ret, va_list ap); @@ -213,10 +253,11 @@ typedef enum JsonDispatchFlags { JSON_PERMISSIVE = 1 << 0, /* Shall parsing errors be considered fatal for this property? */ JSON_MANDATORY = 1 << 1, /* Should existence of this property be mandatory? */ JSON_LOG = 1 << 2, /* Should the parser log about errors? */ + JSON_SAFE = 1 << 3, /* Don't accept "unsafe" strings in json_dispatch_string() + json_dispatch_string() */ /* The following two may be passed into log_json() in addition to the three above */ - JSON_DEBUG = 1 << 3, /* Indicates that this log message is a debug message */ - JSON_WARNING = 1 << 4, /* Indicates that this log message is a warning message */ + JSON_DEBUG = 1 << 4, /* Indicates that this log message is a debug message */ + JSON_WARNING = 1 << 5, /* Indicates that this log message is a warning message */ } JsonDispatchFlags; typedef int (*JsonDispatchCallback)(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); @@ -232,6 +273,7 @@ typedef struct JsonDispatch { int json_dispatch(JsonVariant *v, const JsonDispatch table[], JsonDispatchCallback bad, JsonDispatchFlags flags, void *userdata); int json_dispatch_string(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); +int json_dispatch_const_string(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); int json_dispatch_strv(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); int json_dispatch_boolean(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); int json_dispatch_tristate(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); @@ -240,6 +282,10 @@ int json_dispatch_integer(const char *name, JsonVariant *variant, JsonDispatchFl int json_dispatch_unsigned(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); int json_dispatch_uint32(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); int json_dispatch_int32(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); +int json_dispatch_uid_gid(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); +int json_dispatch_user_group_name(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); +int json_dispatch_id128(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); +int json_dispatch_unsupported(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); assert_cc(sizeof(uintmax_t) == sizeof(uint64_t)); #define json_dispatch_uint64 json_dispatch_unsigned @@ -275,6 +321,9 @@ int json_log_internal(JsonVariant *variant, int level, int error, const char *fi : -ERRNO_VALUE(_e); \ }) +#define json_log_oom(variant, flags) \ + json_log(variant, flags, SYNTHETIC_ERRNO(ENOMEM), "Out of memory.") + #define JSON_VARIANT_STRING_CONST(x) _JSON_VARIANT_STRING_CONST(UNIQ, (x)) #define _JSON_VARIANT_STRING_CONST(xq, x) \ @@ -284,5 +333,7 @@ int json_log_internal(JsonVariant *variant, int level, int error, const char *fi (JsonVariant*) ((uintptr_t) UNIQ_T(json_string_const, xq) + 1); \ }) +int json_variant_unbase64(JsonVariant *v, void **ret, size_t *ret_size); + const char *json_variant_type_to_string(JsonVariantType t); JsonVariantType json_variant_type_from_string(const char *s); diff --git a/src/shared/loop-util.c b/src/shared/loop-util.c index 22525d5c63303..6952a7f654de0 100644 --- a/src/shared/loop-util.c +++ b/src/shared/loop-util.c @@ -2,26 +2,35 @@ #include #include +#include +#include #include +#include #include #include #include "alloc-util.h" #include "fd-util.h" +#include "fileio.h" #include "loop-util.h" +#include "parse-util.h" #include "stat-util.h" -int loop_device_make(int fd, int open_flags, LoopDevice **ret) { - const struct loop_info64 info = { - .lo_flags = LO_FLAGS_AUTOCLEAR|LO_FLAGS_PARTSCAN|(open_flags == O_RDONLY ? LO_FLAGS_READ_ONLY : 0), - }; +int loop_device_make_full( + int fd, + int open_flags, + uint64_t offset, + uint64_t size, + uint32_t loop_flags, + LoopDevice **ret) { _cleanup_close_ int control = -1, loop = -1; _cleanup_free_ char *loopdev = NULL; unsigned n_attempts = 0; + struct loop_info64 info; + LoopDevice *d = NULL; struct stat st; - LoopDevice *d; - int nr, r; + int nr = -1, r; assert(fd >= 0); assert(ret); @@ -31,31 +40,42 @@ int loop_device_make(int fd, int open_flags, LoopDevice **ret) { return -errno; if (S_ISBLK(st.st_mode)) { - int copy; + if (ioctl(loop, LOOP_GET_STATUS64, &info) >= 0) { + /* Oh! This is a loopback device? That's interesting! */ + nr = info.lo_number; - /* If this is already a block device, store a copy of the fd as it is */ + if (asprintf(&loopdev, "/dev/loop%i", nr) < 0) + return -ENOMEM; + } - copy = fcntl(fd, F_DUPFD_CLOEXEC, 3); - if (copy < 0) - return -errno; + if (offset == 0 && IN_SET(size, 0, UINT64_MAX)) { + int copy; - d = new0(LoopDevice, 1); - if (!d) - return -ENOMEM; + /* If this is already a block device, store a copy of the fd as it is */ - *d = (LoopDevice) { - .fd = copy, - .nr = -1, - .relinquished = true, /* It's not allocated by us, don't destroy it when this object is freed */ - }; + copy = fcntl(fd, F_DUPFD_CLOEXEC, 3); + if (copy < 0) + return -errno; - *ret = d; - return d->fd; - } + d = new0(LoopDevice, 1); + if (!d) + return -ENOMEM; - r = stat_verify_regular(&st); - if (r < 0) - return r; + *d = (LoopDevice) { + .fd = copy, + .nr = nr, + .node = TAKE_PTR(loopdev), + .relinquished = true, /* It's not allocated by us, don't destroy it when this object is freed */ + }; + + *ret = d; + return d->fd; + } + } else { + r = stat_verify_regular(&st); + if (r < 0) + return r; + } control = open("/dev/loop-control", O_RDWR|O_CLOEXEC|O_NOCTTY|O_NONBLOCK); if (control < 0) @@ -87,12 +107,23 @@ int loop_device_make(int fd, int open_flags, LoopDevice **ret) { loop = safe_close(loop); } - if (ioctl(loop, LOOP_SET_STATUS64, &info) < 0) - return -errno; + info = (struct loop_info64) { + /* Use the specified flags, but configure the read-only flag from the open flags, and force autoclear */ + .lo_flags = (loop_flags & ~LO_FLAGS_READ_ONLY) | ((loop_flags & O_ACCMODE) == O_RDONLY ? LO_FLAGS_READ_ONLY : 0) | LO_FLAGS_AUTOCLEAR, + .lo_offset = offset, + .lo_sizelimit = size == UINT64_MAX ? 0 : size, + }; + + if (ioctl(loop, LOOP_SET_STATUS64, &info) < 0) { + r = -errno; + goto fail; + } d = new(LoopDevice, 1); - if (!d) - return -ENOMEM; + if (!d) { + r = -ENOMEM; + goto fail; + } *d = (LoopDevice) { .fd = TAKE_FD(loop), @@ -102,9 +133,17 @@ int loop_device_make(int fd, int open_flags, LoopDevice **ret) { *ret = d; return d->fd; + +fail: + if (fd >= 0) + (void) ioctl(fd, LOOP_CLR_FD); + if (d && d->fd >= 0) + (void) ioctl(d->fd, LOOP_CLR_FD); + + return r; } -int loop_device_make_by_path(const char *path, int open_flags, LoopDevice **ret) { +int loop_device_make_by_path(const char *path, int open_flags, uint32_t loop_flags, LoopDevice **ret) { _cleanup_close_ int fd = -1; assert(path); @@ -115,7 +154,7 @@ int loop_device_make_by_path(const char *path, int open_flags, LoopDevice **ret) if (fd < 0) return -errno; - return loop_device_make(fd, open_flags, ret); + return loop_device_make(fd, open_flags, loop_flags, ret); } LoopDevice* loop_device_unref(LoopDevice *d) { @@ -157,3 +196,188 @@ void loop_device_relinquish(LoopDevice *d) { d->relinquished = true; } + +int loop_device_open(const char *loop_path, int open_flags, LoopDevice **ret) { + _cleanup_close_ int loop_fd = -1; + _cleanup_free_ char *p = NULL; + struct loop_info64 info; + struct stat st; + LoopDevice *d; + int nr; + + assert(loop_path); + assert(ret); + + loop_fd = open(loop_path, O_CLOEXEC|O_NONBLOCK|O_NOCTTY|open_flags); + if (loop_fd < 0) + return -errno; + + if (fstat(loop_fd, &st) < 0) + return -errno; + if (!S_ISBLK(st.st_mode)) + return -ENOTBLK; + + if (ioctl(loop_fd, LOOP_GET_STATUS64, &info) >= 0) + nr = info.lo_number; + else + nr = -1; + + p = strdup(loop_path); + if (!p) + return -ENOMEM; + + d = new(LoopDevice, 1); + if (!d) + return -ENOMEM; + + *d = (LoopDevice) { + .fd = TAKE_FD(loop_fd), + .nr = nr, + .node = TAKE_PTR(p), + .relinquished = true, /* It's not ours, don't try to destroy it when this object is freed */ + }; + + *ret = d; + return d->fd; +} + +static int resize_partition(int partition_fd, uint64_t offset, uint64_t size) { + _cleanup_free_ char *sysfs = NULL, *whole = NULL, *buffer = NULL; + uint64_t current_offset, current_size, partno; + _cleanup_close_ int whole_fd = -1; + struct blkpg_ioctl_arg ba; + struct blkpg_partition bp; + struct stat st; + dev_t devno; + int r; + + assert(partition_fd >= 0); + + /* Resizes the partition the loopback device refer to (assuming it refers to one instead of an actual + * loopback device), and changes the offset, if needed. This is a fancy wrapper around + * BLKPG_RESIZE_PARTITION. */ + + if (fstat(partition_fd, &st) < 0) + return -errno; + + assert(S_ISBLK(st.st_mode)); + + if (asprintf(&sysfs, "/sys/dev/block/%u:%u/partition", major(st.st_rdev), minor(st.st_rdev)) < 0) + return -ENOMEM; + + r = read_one_line_file(sysfs, &buffer); + if (r == -ENOENT) /* not a partition, cannot resize */ + return -ENOTTY; + if (r < 0) + return r; + r = safe_atou64(buffer, &partno); + if (r < 0) + return r; + + sysfs = mfree(sysfs); + if (asprintf(&sysfs, "/sys/dev/block/%u:%u/start", major(st.st_rdev), minor(st.st_rdev)) < 0) + return -ENOMEM; + + buffer = mfree(buffer); + r = read_one_line_file(sysfs, &buffer); + if (r < 0) + return r; + r = safe_atou64(buffer, ¤t_offset); + if (r < 0) + return r; + if (current_offset > UINT64_MAX/512U) + return -EINVAL; + current_offset *= 512U; + + if (ioctl(partition_fd, BLKGETSIZE64, ¤t_size) < 0) + return -EINVAL; + + if (size == UINT64_MAX && offset == UINT64_MAX) + return 0; + if (current_size == size && current_offset == offset) + return 0; + + sysfs = mfree(sysfs); + if (asprintf(&sysfs, "/sys/dev/block/%u:%u/../dev", major(st.st_rdev), minor(st.st_rdev)) < 0) + return -ENOMEM; + + buffer = mfree(buffer); + r = read_one_line_file(sysfs, &buffer); + if (r < 0) + return r; + r = parse_dev(buffer, &devno); + if (r < 0) + return r; + + r = device_path_make_major_minor(S_IFBLK, devno, &whole); + if (r < 0) + return r; + + whole_fd = open(whole, O_RDWR|O_CLOEXEC|O_NONBLOCK|O_NOCTTY); + if (whole_fd < 0) + return -errno; + + bp = (struct blkpg_partition) { + .pno = partno, + .start = offset == UINT64_MAX ? current_offset : offset, + .length = size == UINT64_MAX ? current_size : size, + }; + + ba = (struct blkpg_ioctl_arg) { + .op = BLKPG_RESIZE_PARTITION, + .data = &bp, + .datalen = sizeof(bp), + }; + + if (ioctl(whole_fd, BLKPG, &ba) < 0) + return -errno; + + return 0; +} + +int loop_device_refresh_size(LoopDevice *d, uint64_t offset, uint64_t size) { + struct loop_info64 info; + assert(d); + + /* Changes the offset/start of the loop device relative to the beginning of the underlying file or + * block device. If this loop device actually refers to a partition and not a loopback device, we'll + * try to adjust the partition offsets instead. + * + * If either offset or size is UINT64_MAX we won't change that parameter. */ + + if (d->fd < 0) + return -EBADF; + + if (d->nr < 0) /* not a loopback device */ + return resize_partition(d->fd, offset, size); + + if (ioctl(d->fd, LOOP_GET_STATUS64, &info) < 0) + return -errno; + + if (size == UINT64_MAX && offset == UINT64_MAX) + return 0; + if (info.lo_sizelimit == size && info.lo_offset == offset) + return 0; + + if (size != UINT64_MAX) + info.lo_sizelimit = size; + if (offset != UINT64_MAX) + info.lo_offset = offset; + + if (ioctl(d->fd, LOOP_SET_STATUS64, &info) < 0) + return -errno; + + return 0; +} + +int loop_device_flock(LoopDevice *d, int operation) { + assert(d); + + if (d->fd < 0) + return -EBADF; + + if (flock(d->fd, operation) < 0) + return -errno; + + return 0; +} diff --git a/src/shared/loop-util.h b/src/shared/loop-util.h index d78466c5ee683..5156b46ad611a 100644 --- a/src/shared/loop-util.h +++ b/src/shared/loop-util.h @@ -14,10 +14,19 @@ struct LoopDevice { bool relinquished; }; -int loop_device_make(int fd, int open_flags, LoopDevice **ret); -int loop_device_make_by_path(const char *path, int open_flags, LoopDevice **ret); +int loop_device_make_full(int fd, int open_flags, uint64_t offset, uint64_t size, uint32_t loop_flags, LoopDevice **ret); +static inline int loop_device_make(int fd, int open_flags, uint32_t loop_flags, LoopDevice **ret) { + return loop_device_make_full(fd, open_flags, 0, 0, loop_flags, ret); +} + +int loop_device_make_by_path(const char *path, int open_flags, uint32_t loop_flags, LoopDevice **ret); +int loop_device_open(const char *loop_path, int open_flags, LoopDevice **ret); LoopDevice* loop_device_unref(LoopDevice *d); DEFINE_TRIVIAL_CLEANUP_FUNC(LoopDevice*, loop_device_unref); void loop_device_relinquish(LoopDevice *d); + +int loop_device_refresh_size(LoopDevice *d, uint64_t offset, uint64_t size); + +int loop_device_flock(LoopDevice *d, int operation); diff --git a/src/shared/machine-image.c b/src/shared/machine-image.c index 7007374192796..4e0bea41d1a57 100644 --- a/src/shared/machine-image.c +++ b/src/shared/machine-image.c @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -10,7 +12,6 @@ #include #include #include -#include #include "alloc-util.h" #include "btrfs-util.h" @@ -1168,7 +1169,7 @@ int image_read_metadata(Image *i) { _cleanup_(loop_device_unrefp) LoopDevice *d = NULL; _cleanup_(dissected_image_unrefp) DissectedImage *m = NULL; - r = loop_device_make_by_path(i->path, O_RDONLY, &d); + r = loop_device_make_by_path(i->path, O_RDONLY, LO_FLAGS_PARTSCAN, &d); if (r < 0) return r; diff --git a/src/shared/main-func.h b/src/shared/main-func.h index 6c26cb9fb5673..cf23ad450c5ce 100644 --- a/src/shared/main-func.h +++ b/src/shared/main-func.h @@ -3,6 +3,8 @@ #include +#include "sd-daemon.h" + #include "pager.h" #include "selinux-util.h" #include "spawn-ask-password-agent.h" @@ -16,6 +18,8 @@ save_argc_argv(argc, argv); \ intro; \ r = impl; \ + if (r < 0) \ + (void) sd_notifyf(0, "ERRNO=%i", -r); \ ask_password_agent_close(); \ polkit_agent_close(); \ pager_close(); \ diff --git a/src/shared/meson.build b/src/shared/meson.build index e9005a30e3a52..1dc6a721c4461 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -35,6 +35,8 @@ shared_sources = files(''' calendarspec.h cgroup-show.c cgroup-show.h + chown-recursive.c + chown-recursive.h clean-ipc.c clean-ipc.h clock-util.c @@ -82,6 +84,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 @@ -145,6 +153,8 @@ shared_sources = files(''' ptyfwd.h reboot-util.c reboot-util.h + resize-fs.c + resize-fs.h resolve-util.c resolve-util.h seccomp-util.h @@ -174,6 +184,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 @@ -218,6 +236,10 @@ 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') +endif + generate_ip_protocol_list = find_program('generate-ip-protocol-list.sh') ip_protocol_list_txt = custom_target( 'ip-protocol-list.txt', @@ -274,7 +296,8 @@ libshared_deps = [threads, libidn, libxz, liblz4, - libblkid] + libblkid, + libpam] libshared_sym_path = '@0@/libshared.sym'.format(meson.current_source_dir()) diff --git a/src/shared/pam-util.c b/src/shared/pam-util.c new file mode 100644 index 0000000000000..f000798ce0683 --- /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 0000000000000..26d07b7f0cbed --- /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/resize-fs.c b/src/shared/resize-fs.c new file mode 100644 index 0000000000000..9f33dd77d88a3 --- /dev/null +++ b/src/shared/resize-fs.c @@ -0,0 +1,112 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include +#include +#include + +#include "blockdev-util.h" +#include "fs-util.h" +#include "missing_fs.h" +#include "missing_magic.h" +#include "missing_xfs.h" +#include "resize-fs.h" +#include "stat-util.h" + +int resize_fs(int fd, uint64_t sz) { + struct statfs sfs; + int r; + + assert(fd >= 0); + + /* Rounds down to next block size */ + + if (sz <= 0 || sz == UINT64_MAX) + return -ERANGE; + + if (fstatfs(fd, &sfs) < 0) + return -errno; + + if (is_fs_type(&sfs, EXT4_SUPER_MAGIC)) { + uint64_t u; + + if (sz < EXT4_MINIMAL_SIZE) + return -ERANGE; + + u = sz / sfs.f_bsize; + + if (ioctl(fd, EXT4_IOC_RESIZE_FS, &u) < 0) + return -errno; + + } else if (is_fs_type(&sfs, BTRFS_SUPER_MAGIC)) { + struct btrfs_ioctl_vol_args args = {}; + + /* 256M is the minimize size enforced by the btrfs kernel code when resizing (which is + * strange btw, as mkfs.btrfs is fine creating file systems > 109M). It will return EINVAL in + * that case, let's catch this error beforehand though, and report a more explanatory + * error. */ + + if (sz < BTRFS_MINIMAL_SIZE) + return -ERANGE; + + r = snprintf(args.name, sizeof(args.name), "%" PRIu64, sz); + assert((size_t) r < sizeof(args.name)); + + if (ioctl(fd, BTRFS_IOC_RESIZE, &args) < 0) + return -errno; + + } else if (is_fs_type(&sfs, XFS_SB_MAGIC)) { + xfs_fsop_geom_t geo; + xfs_growfs_data_t d; + + if (sz < XFS_MINIMAL_SIZE) + return -ERANGE; + + if (ioctl(fd, XFS_IOC_FSGEOMETRY, &geo) < 0) + return -errno; + + d = (xfs_growfs_data_t) { + .imaxpct = geo.imaxpct, + .newblocks = sz / geo.blocksize, + }; + + if (ioctl(fd, XFS_IOC_FSGROWFSDATA, &d) < 0) + return -errno; + + } else + return -EOPNOTSUPP; + + return 0; +} + +uint64_t minimal_size_by_fs_magic(statfs_f_type_t magic) { + + switch (magic) { + + case (statfs_f_type_t) EXT4_SUPER_MAGIC: + return EXT4_MINIMAL_SIZE; + + case (statfs_f_type_t) XFS_SB_MAGIC: + return XFS_MINIMAL_SIZE; + + case (statfs_f_type_t) BTRFS_SUPER_MAGIC: + return BTRFS_MINIMAL_SIZE; + + default: + return UINT64_MAX; + } +} + +uint64_t minimal_size_by_fs_name(const char *name) { + + if (streq_ptr(name, "ext4")) + return EXT4_MINIMAL_SIZE; + + if (streq_ptr(name, "xfs")) + return XFS_MINIMAL_SIZE; + + if (streq_ptr(name, "btrfs")) + return BTRFS_MINIMAL_SIZE; + + return UINT64_MAX; +} diff --git a/src/shared/resize-fs.h b/src/shared/resize-fs.h new file mode 100644 index 0000000000000..b5441765289ec --- /dev/null +++ b/src/shared/resize-fs.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +#include "stat-util.h" + +int resize_fs(int fd, uint64_t sz); + +#define BTRFS_MINIMAL_SIZE (256U*1024U*1024U) +#define XFS_MINIMAL_SIZE (14U*1024U*1024U) +#define EXT4_MINIMAL_SIZE (1024U*1024U) + +uint64_t minimal_size_by_fs_magic(statfs_f_type_t magic); +uint64_t minimal_size_by_fs_name(const char *str); diff --git a/src/shared/user-record-nss.c b/src/shared/user-record-nss.c new file mode 100644 index 0000000000000..2d881fec8a0ee --- /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 "strv.h" +#include "user-record-nss.h" +#include "user-util.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 0000000000000..d5fb23ad2a294 --- /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 0000000000000..80f89e0acc3e0 --- /dev/null +++ b/src/shared/user-record-show.c @@ -0,0 +1,414 @@ +/* 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; + 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; + }} + + 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: %zubit\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->hashed_password)) + printf(" Passwords: %zu\n", strv_length(hr->hashed_password)); + + 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 0000000000000..bd22be2ae04ad --- /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 0000000000000..f5f9a9375409d --- /dev/null +++ b/src/shared/user-record.c @@ -0,0 +1,1680 @@ +/* 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 "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 10 +#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, + }; + + return h; +} + +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); + + free(h->fscrypt_salt); + + 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); + + 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; + _cleanup_free_ char *k = NULL; + 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; + _cleanup_free_ char *k = NULL; + 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; + _cleanup_free_ char *k = NULL; + 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; + _cleanup_free_ char *k = NULL; + 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; + _cleanup_free_ char *k = NULL; + 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 }, + {}, + }; + + return json_dispatch(variant, secret_dispatch_table, NULL, flags, userdata); +} + +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 }, + {}, + }; + + return json_dispatch(variant, privileged_dispatch_table, NULL, flags, userdata); +} + +static int dispatch_fscrypt_salt(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + UserRecord *h = userdata; + size_t l; + void *b; + int r; + + assert_se(h); + + if (json_variant_is_null(variant)) { + h->fscrypt_salt = mfree(h->fscrypt_salt); + h->fscrypt_salt_size = 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 fscrypt salt: %m"); + + free(h->fscrypt_salt); + h->fscrypt_salt = b; + h->fscrypt_salt_size = l; + + return 0; +} + +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 }, + { "fscryptSalt", _JSON_VARIANT_TYPE_INVALID, dispatch_fscrypt_salt, 0, 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 }, + {}, + }; + + char smid[SD_ID128_STRING_MAX]; + JsonVariant *m; + sd_id128_t mid; + int r; + + 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 }, + { "fscryptSalt", _JSON_VARIANT_TYPE_INVALID, dispatch_fscrypt_salt, 0, 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 }, + {}, + }; + + 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.", 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 (!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"); + + v = w; + 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)) { + _cleanup_(json_variant_unrefp) JsonVariant *k = NULL; + + /* If we are supposed to strip regular items, then let's instead just allocate a new object + * with just the stuff we need. */ + + r = json_variant_new_object(&k, array, n_retain); + if (r < 0) + return json_log(v, json_flags, r, "Failed to allocate new object: %m"); + + json_variant_unref(w); + w = TAKE_PTR(k); + + v = w; + } 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 }, + { "fscryptSalt", _JSON_VARIANT_TYPE_INVALID, dispatch_fscrypt_salt, 0, 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 }, + + { "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); + JsonVariant *per_machine, *binding, *status; + 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. */ + + per_machine = json_variant_by_key(h->json, "perMachine"); + if (per_machine) { + r = dispatch_per_machine("perMachine", per_machine, json_flags, h); + if (r < 0) + return r; + } + + binding = json_variant_by_key(h->json, "binding"); + if (binding) { + r = dispatch_binding("binding", binding, json_flags, h); + if (r < 0) + return r; + } + + status = json_variant_by_key(h->json, "status"); + if (status) { + r = dispatch_status("binding", 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") || + 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; +} + +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; +} + +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 0000000000000..83ef1b3f33c27 --- /dev/null +++ b/src/shared/user-record.h @@ -0,0 +1,356 @@ +/* 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 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; + + void *fscrypt_salt; + size_t fscrypt_salt_size; + + 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, we currently + * do not actually make use of these in the systemd codebase */ + 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) */ + + 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_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); + +/* 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 0000000000000..eef435d5a5eae --- /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; +} + +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: { + struct user_data { + JsonVariant *record; + bool incomplete; + } user_data = {}; + + static const JsonDispatch dispatch_table[] = { + { "record", _JSON_VARIANT_TYPE_INVALID, json_dispatch_variant, offsetof(struct user_data, record), 0 }, + { "incomplete", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(struct user_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: { + struct group_data { + JsonVariant *record; + bool incomplete; + } group_data = {}; + + static const JsonDispatch dispatch_table[] = { + { "record", _JSON_VARIANT_TYPE_INVALID, json_dispatch_variant, offsetof(struct group_data, record), 0 }, + { "incomplete", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(struct 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)) != 0 && 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 0000000000000..4288b0ff95dfc --- /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/shared/varlink.c b/src/shared/varlink.c index 99343167f66c2..7f59fde4d8c2b 100644 --- a/src/shared/varlink.c +++ b/src/shared/varlink.c @@ -29,6 +29,7 @@ typedef enum VarlinkState { /* Client side states */ VARLINK_IDLE_CLIENT, VARLINK_AWAITING_REPLY, + VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING, VARLINK_CALLED, VARLINK_PROCESSING_REPLY, @@ -39,7 +40,6 @@ typedef enum VarlinkState { VARLINK_PROCESSING_METHOD_MORE, VARLINK_PROCESSING_METHOD_ONEWAY, VARLINK_PROCESSED_METHOD, - VARLINK_PROCESSED_METHOD_MORE, VARLINK_PENDING_METHOD, VARLINK_PENDING_METHOD_MORE, @@ -63,6 +63,7 @@ typedef enum VarlinkState { IN_SET(state, \ VARLINK_IDLE_CLIENT, \ VARLINK_AWAITING_REPLY, \ + VARLINK_AWAITING_REPLY_MORE, \ VARLINK_CALLING, \ VARLINK_CALLED, \ VARLINK_PROCESSING_REPLY, \ @@ -71,7 +72,6 @@ typedef enum VarlinkState { VARLINK_PROCESSING_METHOD_MORE, \ VARLINK_PROCESSING_METHOD_ONEWAY, \ VARLINK_PROCESSED_METHOD, \ - VARLINK_PROCESSED_METHOD_MORE, \ VARLINK_PENDING_METHOD, \ VARLINK_PENDING_METHOD_MORE) @@ -185,6 +185,7 @@ struct VarlinkServer { static const char* const varlink_state_table[_VARLINK_STATE_MAX] = { [VARLINK_IDLE_CLIENT] = "idle-client", [VARLINK_AWAITING_REPLY] = "awaiting-reply", + [VARLINK_AWAITING_REPLY_MORE] = "awaiting-reply-more", [VARLINK_CALLING] = "calling", [VARLINK_CALLED] = "called", [VARLINK_PROCESSING_REPLY] = "processing-reply", @@ -193,7 +194,6 @@ static const char* const varlink_state_table[_VARLINK_STATE_MAX] = { [VARLINK_PROCESSING_METHOD_MORE] = "processing-method-more", [VARLINK_PROCESSING_METHOD_ONEWAY] = "processing-method-oneway", [VARLINK_PROCESSED_METHOD] = "processed-method", - [VARLINK_PROCESSED_METHOD_MORE] = "processed-method-more", [VARLINK_PENDING_METHOD] = "pending-method", [VARLINK_PENDING_METHOD_MORE] = "pending-method-more", [VARLINK_PENDING_DISCONNECT] = "pending-disconnect", @@ -287,6 +287,8 @@ int varlink_connect_address(Varlink **ret, const char *address) { if (v->fd < 0) return -errno; + v->fd = fd_move_above_stdio(v->fd); + if (connect(v->fd, &sockaddr.sa, SOCKADDR_UN_LEN(sockaddr.un)) < 0) { if (!IN_SET(errno, EAGAIN, EINPROGRESS)) return -errno; @@ -405,7 +407,7 @@ static int varlink_test_disconnect(Varlink *v) { goto disconnect; /* If we are waiting for incoming data but the read side is shut down, disconnect. */ - if (IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_CALLING, VARLINK_IDLE_SERVER) && v->read_disconnected) + if (IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING, VARLINK_IDLE_SERVER) && v->read_disconnected) goto disconnect; /* Similar, if are a client that hasn't written anything yet but the write side is dead, also @@ -478,7 +480,7 @@ static int varlink_read(Varlink *v) { assert(v); - if (!IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_CALLING, VARLINK_IDLE_SERVER)) + if (!IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING, VARLINK_IDLE_SERVER)) return 0; if (v->connecting) /* read() on a socket while we are in connect() will fail with EINVAL, hence exit early here */ return 0; @@ -578,7 +580,7 @@ static int varlink_parse_message(Varlink *v) { varlink_log(v, "New incoming message: %s", begin); - r = json_parse(begin, &v->current, NULL, NULL); + r = json_parse(begin, 0, &v->current, NULL, NULL); if (r < 0) return r; @@ -596,7 +598,7 @@ static int varlink_parse_message(Varlink *v) { static int varlink_test_timeout(Varlink *v) { assert(v); - if (!IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_CALLING)) + if (!IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING)) return 0; if (v->timeout == USEC_INFINITY) return 0; @@ -673,7 +675,7 @@ static int varlink_dispatch_reply(Varlink *v) { assert(v); - if (!IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_CALLING)) + if (!IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING)) return 0; if (!v->current) return 0; @@ -715,6 +717,11 @@ static int varlink_dispatch_reply(Varlink *v) { goto invalid; } + /* Replies with 'continue' set are only OK we set 'more' when we initiated the method */ + if (v->state != VARLINK_AWAITING_REPLY_MORE && FLAGS_SET(flags, VARLINK_REPLY_CONTINUES)) + goto invalid; + + /* An error is final */ if (error && FLAGS_SET(flags, VARLINK_REPLY_CONTINUES)) goto invalid; @@ -722,7 +729,7 @@ static int varlink_dispatch_reply(Varlink *v) { if (r < 0) goto invalid; - if (v->state == VARLINK_AWAITING_REPLY) { + if (IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE)) { varlink_set_state(v, VARLINK_PROCESSING_REPLY); if (v->reply_callback) { @@ -734,17 +741,18 @@ static int varlink_dispatch_reply(Varlink *v) { v->current = json_variant_unref(v->current); if (v->state == VARLINK_PROCESSING_REPLY) { + assert(v->n_pending > 0); - v->n_pending--; - varlink_set_state(v, v->n_pending == 0 ? VARLINK_IDLE_CLIENT : VARLINK_AWAITING_REPLY); + if (!FLAGS_SET(flags, VARLINK_REPLY_CONTINUES)) + v->n_pending--; + + varlink_set_state(v, + FLAGS_SET(flags, VARLINK_REPLY_CONTINUES) ? VARLINK_AWAITING_REPLY_MORE : + v->n_pending == 0 ? VARLINK_IDLE_CLIENT : VARLINK_AWAITING_REPLY); } } else { assert(v->state == VARLINK_CALLING); - - if (FLAGS_SET(flags, VARLINK_REPLY_CONTINUES)) - goto invalid; - varlink_set_state(v, VARLINK_CALLED); } @@ -878,7 +886,6 @@ static int varlink_dispatch_method(Varlink *v) { varlink_set_state(v, VARLINK_PENDING_METHOD); break; - case VARLINK_PROCESSED_METHOD_MORE: /* One reply for a "more" message was sent, more to come */ case VARLINK_PROCESSING_METHOD_MORE: /* No reply for a "more" message was sent, more to come */ varlink_set_state(v, VARLINK_PENDING_METHOD_MORE); break; @@ -1073,7 +1080,7 @@ int varlink_get_events(Varlink *v) { return EPOLLOUT; if (!v->read_disconnected && - IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_CALLING, VARLINK_IDLE_SERVER) && + IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING, VARLINK_IDLE_SERVER) && !v->current && v->input_buffer_unscanned <= 0) ret |= EPOLLIN; @@ -1091,7 +1098,7 @@ int varlink_get_timeout(Varlink *v, usec_t *ret) { if (v->state == VARLINK_DISCONNECTED) return -ENOTCONN; - if (IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_CALLING) && + if (IN_SET(v->state, VARLINK_AWAITING_REPLY, VARLINK_AWAITING_REPLY_MORE, VARLINK_CALLING) && v->timeout != USEC_INFINITY) { if (ret) *ret = usec_add(v->timestamp, v->timeout); @@ -1189,6 +1196,15 @@ int varlink_close(Varlink *v) { return 1; } +Varlink* varlink_close_unref(Varlink *v) { + + if (!v) + return NULL; + + (void) varlink_close(v); + return varlink_unref(v); +} + Varlink* varlink_flush_close_unref(Varlink *v) { if (!v) @@ -1196,7 +1212,6 @@ Varlink* varlink_flush_close_unref(Varlink *v) { (void) varlink_flush(v); (void) varlink_close(v); - return varlink_unref(v); } @@ -1259,6 +1274,8 @@ int varlink_send(Varlink *v, const char *method, JsonVariant *parameters) { if (v->state == VARLINK_DISCONNECTED) return -ENOTCONN; + + /* We allow enqueuing multiple method calls at once! */ if (!IN_SET(v->state, VARLINK_IDLE_CLIENT, VARLINK_AWAITING_REPLY)) return -EBUSY; @@ -1308,6 +1325,8 @@ int varlink_invoke(Varlink *v, const char *method, JsonVariant *parameters) { if (v->state == VARLINK_DISCONNECTED) return -ENOTCONN; + + /* We allow enqueing multiple method calls at once! */ if (!IN_SET(v->state, VARLINK_IDLE_CLIENT, VARLINK_AWAITING_REPLY)) return -EBUSY; @@ -1349,6 +1368,60 @@ int varlink_invokeb(Varlink *v, const char *method, ...) { return varlink_invoke(v, method, parameters); } +int varlink_observe(Varlink *v, const char *method, JsonVariant *parameters) { + _cleanup_(json_variant_unrefp) JsonVariant *m = NULL; + int r; + + assert_return(v, -EINVAL); + assert_return(method, -EINVAL); + + if (v->state == VARLINK_DISCONNECTED) + return -ENOTCONN; + /* Note that we don't allow enqueuing multiple method calls when we are in more/continues mode! We + * thus insist on an idle client here. */ + if (v->state != VARLINK_IDLE_CLIENT) + return -EBUSY; + + r = varlink_sanitize_parameters(¶meters); + if (r < 0) + return r; + + r = json_build(&m, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("method", JSON_BUILD_STRING(method)), + JSON_BUILD_PAIR("parameters", JSON_BUILD_VARIANT(parameters)), + JSON_BUILD_PAIR("more", JSON_BUILD_BOOLEAN(true)))); + if (r < 0) + return r; + + r = varlink_enqueue_json(v, m); + if (r < 0) + return r; + + + varlink_set_state(v, VARLINK_AWAITING_REPLY_MORE); + v->n_pending++; + v->timestamp = now(CLOCK_MONOTONIC); + + return 0; +} + +int varlink_observeb(Varlink *v, const char *method, ...) { + _cleanup_(json_variant_unrefp) JsonVariant *parameters = NULL; + va_list ap; + int r; + + assert_return(v, -EINVAL); + + va_start(ap, method); + r = json_buildv(¶meters, ap); + va_end(ap); + + if (r < 0) + return r; + + return varlink_observe(v, method, parameters); +} + int varlink_call( Varlink *v, const char *method, @@ -2157,6 +2230,8 @@ int varlink_server_listen_address(VarlinkServer *s, const char *address, mode_t if (fd < 0) return -errno; + fd = fd_move_above_stdio(fd); + (void) sockaddr_un_unlink(&sockaddr.un); RUN_WITH_UMASK(~m & 0777) @@ -2339,17 +2414,18 @@ int varlink_server_bind_connect(VarlinkServer *s, VarlinkConnect callback) { } unsigned varlink_server_connections_max(VarlinkServer *s) { - struct rlimit rl; + int dts; /* If a server is specified, return the setting for that server, otherwise the default value */ if (s) return s->connections_max; - assert_se(getrlimit(RLIMIT_NOFILE, &rl) >= 0); + dts = getdtablesize(); + assert_se(dts > 0); /* Make sure we never use up more than ¾th of RLIMIT_NOFILE for IPC */ - if (VARLINK_DEFAULT_CONNECTIONS_MAX > rl.rlim_cur / 4 * 3) - return rl.rlim_cur / 4 * 3; + if (VARLINK_DEFAULT_CONNECTIONS_MAX > (unsigned) dts / 4 * 3) + return dts / 4 * 3; return VARLINK_DEFAULT_CONNECTIONS_MAX; } diff --git a/src/shared/varlink.h b/src/shared/varlink.h index d96fa93619a7e..0d9617d40352f 100644 --- a/src/shared/varlink.h +++ b/src/shared/varlink.h @@ -73,6 +73,7 @@ int varlink_flush(Varlink *v); int varlink_close(Varlink *v); Varlink* varlink_flush_close_unref(Varlink *v); +Varlink* varlink_close_unref(Varlink *v); /* Enqueue method call, not expecting a reply */ int varlink_send(Varlink *v, const char *method, JsonVariant *parameters); @@ -86,6 +87,10 @@ int varlink_callb(Varlink *v, const char *method, JsonVariant **ret_parameters, int varlink_invoke(Varlink *v, const char *method, JsonVariant *parameters); int varlink_invokeb(Varlink *v, const char *method, ...); +/* Enqueue method call, expect a reply now, and possibly more later, which are all delivered to the reply callback */ +int varlink_observe(Varlink *v, const char *method, JsonVariant *parameters); +int varlink_observeb(Varlink *v, const char *method, ...); + /* Enqueue a final reply */ int varlink_reply(Varlink *v, JsonVariant *parameters); int varlink_replyb(Varlink *v, ...); @@ -148,6 +153,7 @@ int varlink_server_set_connections_max(VarlinkServer *s, unsigned m); int varlink_server_set_description(VarlinkServer *s, const char *description); DEFINE_TRIVIAL_CLEANUP_FUNC(Varlink *, varlink_unref); +DEFINE_TRIVIAL_CLEANUP_FUNC(Varlink *, varlink_close_unref); DEFINE_TRIVIAL_CLEANUP_FUNC(Varlink *, varlink_flush_close_unref); DEFINE_TRIVIAL_CLEANUP_FUNC(VarlinkServer *, varlink_server_unref); diff --git a/src/sleep/sleep.c b/src/sleep/sleep.c index b9fe96635d22b..3b0f4f462424b 100644 --- a/src/sleep/sleep.c +++ b/src/sleep/sleep.c @@ -18,11 +18,12 @@ #include "sd-messages.h" #include "btrfs-util.h" +#include "bus-error.h" #include "def.h" #include "exec-util.h" #include "fd-util.h" -#include "format-util.h" #include "fileio.h" +#include "format-util.h" #include "log.h" #include "main-func.h" #include "parse-util.h" @@ -182,6 +183,49 @@ static int configure_hibernation(void) { return write_hibernate_location_info(); } +static int lock_all_homes(void) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + int r; + + /* Let's synchronously lock all home directories managed by homed that have been marked for it. This + * way the key material required to access these volumes is hopefully removed from memory. */ + + r = sd_bus_open_system(&bus); + if (r < 0) + return log_warning_errno(r, "Failed to connect to system bus, ignoring: %m"); + + r = sd_bus_message_new_method_call( + bus, + &m, + "org.freedesktop.home1", + "/org/freedesktop/home1", + "org.freedesktop.home1.Manager", + "LockAllHomes"); + if (r < 0) + return bus_log_create_error(r); + + /* If homed is not running it can't have any home directories active either. */ + r = sd_bus_message_set_auto_start(m, false); + if (r < 0) + return log_error_errno(r, "Failed to disable auto-start of LockAllHomes() message: %m"); + + r = sd_bus_call(bus, m, DEFAULT_TIMEOUT_USEC, &error, NULL); + if (r < 0) { + if (sd_bus_error_has_name(&error, SD_BUS_ERROR_SERVICE_UNKNOWN) || + sd_bus_error_has_name(&error, SD_BUS_ERROR_NAME_HAS_NO_OWNER)) { + log_debug("systemd-homed is not running, skipping locking of home."); + return 0; + } + + return log_error_errno(r, "Failed to lock home: %s", bus_error_message(&error, r)); + } + + log_debug("Successfully requested that all home directories shall be suspended."); + return 0; +} + static int execute(char **modes, char **states) { char *arguments[] = { NULL, @@ -217,6 +261,7 @@ static int execute(char **modes, char **states) { } (void) execute_directories(dirs, DEFAULT_TIMEOUT_USEC, NULL, NULL, arguments, NULL, EXEC_DIR_PARALLEL | EXEC_DIR_IGNORE_ERRORS); + (void) lock_all_homes(); log_struct(LOG_INFO, "MESSAGE_ID=" SD_MESSAGE_SLEEP_START_STR, diff --git a/src/systemd/sd-bus-vtable.h b/src/systemd/sd-bus-vtable.h index 0f43554d82519..95b5914236a47 100644 --- a/src/systemd/sd-bus-vtable.h +++ b/src/systemd/sd-bus-vtable.h @@ -43,6 +43,7 @@ enum { SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE = 1ULL << 5, SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION = 1ULL << 6, SD_BUS_VTABLE_PROPERTY_EXPLICIT = 1ULL << 7, + SD_BUS_VTABLE_SENSITIVE = 1ULL << 8, /* covers both directions: method call + reply */ _SD_BUS_VTABLE_CAPABILITY_MASK = 0xFFFFULL << 40 }; diff --git a/src/systemd/sd-bus.h b/src/systemd/sd-bus.h index 84ceb62dc79c7..3c9792e4975e2 100644 --- a/src/systemd/sd-bus.h +++ b/src/systemd/sd-bus.h @@ -328,6 +328,7 @@ int sd_bus_message_peek_type(sd_bus_message *m, char *type, const char **content int sd_bus_message_verify_type(sd_bus_message *m, char type, const char *contents); int sd_bus_message_at_end(sd_bus_message *m, int complete); int sd_bus_message_rewind(sd_bus_message *m, int complete); +int sd_bus_message_sensitive(sd_bus_message *m); /* Bus management */ diff --git a/src/test/test-dissect-image.c b/src/test/test-dissect-image.c index 7b32e8373f920..12685dad1373d 100644 --- a/src/test/test-dissect-image.c +++ b/src/test/test-dissect-image.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ #include +#include #include #include "dissect-image.h" @@ -21,7 +22,7 @@ int main(int argc, char *argv[]) { return EXIT_FAILURE; } - r = loop_device_make_by_path(argv[1], O_RDONLY, &d); + r = loop_device_make_by_path(argv[1], O_RDONLY, LO_FLAGS_PARTSCAN, &d); if (r < 0) { log_error_errno(r, "Failed to set up loopback device: %m"); return EXIT_FAILURE; diff --git a/src/test/test-json.c b/src/test/test-json.c index a6613043b9240..c72cd7427432c 100644 --- a/src/test/test-json.c +++ b/src/test/test-json.c @@ -82,7 +82,7 @@ static void test_variant(const char *data, Test test) { _cleanup_free_ char *s = NULL; int r; - r = json_parse(data, &v, NULL, NULL); + r = json_parse(data, 0, &v, NULL, NULL); assert_se(r == 0); assert_se(v); @@ -93,7 +93,7 @@ static void test_variant(const char *data, Test test) { log_info("formatted normally: %s\n", s); - r = json_parse(data, &w, NULL, NULL); + r = json_parse(data, JSON_PARSE_SENSITIVE, &w, NULL, NULL); assert_se(r == 0); assert_se(w); assert_se(json_variant_has_type(v, json_variant_type(w))); @@ -110,7 +110,7 @@ static void test_variant(const char *data, Test test) { log_info("formatted prettily:\n%s", s); - r = json_parse(data, &w, NULL, NULL); + r = json_parse(data, 0, &w, NULL, NULL); assert_se(r == 0); assert_se(w); @@ -302,7 +302,7 @@ static void test_build(void) { assert_se(json_variant_format(a, 0, &s) >= 0); log_info("GOT: %s\n", s); - assert_se(json_parse(s, &b, NULL, NULL) >= 0); + assert_se(json_parse(s, 0, &b, NULL, NULL) >= 0); assert_se(json_variant_equal(a, b)); a = json_variant_unref(a); @@ -313,7 +313,7 @@ static void test_build(void) { s = mfree(s); assert_se(json_variant_format(a, 0, &s) >= 0); log_info("GOT: %s\n", s); - assert_se(json_parse(s, &b, NULL, NULL) >= 0); + assert_se(json_parse(s, 0, &b, NULL, NULL) >= 0); assert_se(json_variant_format(b, 0, &t) >= 0); log_info("GOT: %s\n", t); @@ -365,7 +365,7 @@ static void test_source(void) { assert_se(f = fmemopen_unlocked((void*) data, strlen(data), "r")); - assert_se(json_parse_file(f, "waldo", &v, NULL, NULL) >= 0); + assert_se(json_parse_file(f, "waldo", 0, &v, NULL, NULL) >= 0); printf("--- non-pretty begin ---\n"); json_variant_dump(v, 0, stdout, NULL); @@ -415,6 +415,93 @@ static void test_depth(void) { fputs("\n", stdout); } +static void test_normalize(void) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL; + _cleanup_free_ char *t = NULL; + + assert_se(json_build(&v, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("b", JSON_BUILD_STRING("x")), + JSON_BUILD_PAIR("c", JSON_BUILD_STRING("y")), + JSON_BUILD_PAIR("a", JSON_BUILD_STRING("z")))) >= 0); + + assert_se(!json_variant_is_sorted(v)); + assert_se(!json_variant_is_normalized(v)); + + assert_se(json_variant_format(v, 0, &t) >= 0); + assert_se(streq(t, "{\"b\":\"x\",\"c\":\"y\",\"a\":\"z\"}")); + t = mfree(t); + + assert_se(json_build(&w, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("bar", JSON_BUILD_STRING("zzz")), + JSON_BUILD_PAIR("foo", JSON_BUILD_VARIANT(v)))) >= 0); + + assert_se(json_variant_is_sorted(w)); + assert_se(!json_variant_is_normalized(w)); + + assert_se(json_variant_format(w, 0, &t) >= 0); + assert_se(streq(t, "{\"bar\":\"zzz\",\"foo\":{\"b\":\"x\",\"c\":\"y\",\"a\":\"z\"}}")); + t = mfree(t); + + assert_se(json_variant_sort(&v) >= 0); + assert_se(json_variant_is_sorted(v)); + assert_se(json_variant_is_normalized(v)); + + assert_se(json_variant_format(v, 0, &t) >= 0); + assert_se(streq(t, "{\"a\":\"z\",\"b\":\"x\",\"c\":\"y\"}")); + t = mfree(t); + + assert_se(json_variant_normalize(&w) >= 0); + assert_se(json_variant_is_sorted(w)); + assert_se(json_variant_is_normalized(w)); + + assert_se(json_variant_format(w, 0, &t) >= 0); + assert_se(streq(t, "{\"bar\":\"zzz\",\"foo\":{\"a\":\"z\",\"b\":\"x\",\"c\":\"y\"}}")); + t = mfree(t); +} + +static void test_bisect(void) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + char c; + + /* Tests the bisection logic in json_variant_by_key() */ + + for (c = 'z'; c >= 'a'; c--) { + + if ((c % 3) == 0) + continue; + + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + assert_se(json_variant_new_stringn(&w, (char[4]) { '<', c, c, '>' }, 4) >= 0); + assert_se(json_variant_set_field(&v, (char[2]) { c, 0 }, w) >= 0); + } + + json_variant_dump(v, JSON_FORMAT_COLOR|JSON_FORMAT_PRETTY, NULL, NULL); + + assert_se(!json_variant_is_sorted(v)); + assert_se(!json_variant_is_normalized(v)); + assert_se(json_variant_normalize(&v) >= 0); + assert_se(json_variant_is_sorted(v)); + assert_se(json_variant_is_normalized(v)); + + json_variant_dump(v, JSON_FORMAT_COLOR|JSON_FORMAT_PRETTY, NULL, NULL); + + for (c = 'a'; c <= 'z'; c++) { + JsonVariant *k; + const char *z; + + k = json_variant_by_key(v, (char[2]) { c, 0 }); + assert_se(!k == ((c % 3) == 0)); + + if (!k) + continue; + + assert_se(json_variant_is_string(k)); + + z = (char[5]){ '<', c, c, '>', 0}; + assert_se(streq(json_variant_string(k), z)); + } +} + int main(int argc, char *argv[]) { test_setup_logging(LOG_DEBUG); @@ -462,10 +549,11 @@ int main(int argc, char *argv[]) { test_variant("[ 0, -0, 0.0, -0.0, 0.000, -0.000, 0e0, -0e0, 0e+0, -0e-0, 0e-0, -0e000, 0e+000 ]", test_zeroes); test_build(); - test_source(); - test_depth(); + test_normalize(); + test_bisect(); + return 0; } diff --git a/src/userdb/meson.build b/src/userdb/meson.build new file mode 100644 index 0000000000000..2f7e1accbf90d --- /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 0000000000000..8b10503f15730 --- /dev/null +++ b/src/userdb/userdbctl.c @@ -0,0 +1,753 @@ +/* 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 { + DISPLAY_CLASSIC, + DISPLAY_TABLE, + DISPLAY_FRIENDLY, + DISPLAY_JSON, + _DISPLAY_INVALID = -1 +} arg_display = _DISPLAY_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_display) { + + case DISPLAY_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 DISPLAY_JSON: + json_variant_dump(ur->json, JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_PRETTY, NULL, 0); + break; + + case DISPLAY_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 DISPLAY_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 display 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_display < 0) + arg_display = argc > 1 ? DISPLAY_FRIENDLY : DISPLAY_TABLE; + + if (arg_display == DISPLAY_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_display == DISPLAY_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_display == DISPLAY_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_display) { + + case DISPLAY_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 DISPLAY_JSON: + json_variant_dump(gr->json, JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_PRETTY, NULL, 0); + break; + + case DISPLAY_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 DISPLAY_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_display < 0) + arg_display = argc > 1 ? DISPLAY_FRIENDLY : DISPLAY_TABLE; + + if (arg_display == DISPLAY_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_display == DISPLAY_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_display == DISPLAY_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_display) { + + case DISPLAY_CLASSIC: + /* Strictly speaking there's no 'classic' output for this concept, but let's output it in + * similar style to the classic display for user/group info */ + + printf("%s:%s\n", user, group); + break; + + case DISPLAY_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 DISPLAY_FRIENDLY: + /* Hmm, this is not particularly friendly, but not sure how we could do this better */ + printf("%s: %s\n", group, user); + break; + + case DISPLAY_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 display 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_display < 0) + arg_display = DISPLAY_TABLE; + + if (arg_display == DISPLAY_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", "connectible"); + 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_display == DISPLAY_JSON) + table_print_json(t, NULL, JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO); + else + table_print(t, NULL); + + return 0; +} + +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" + "Show user and group information.\n\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" + " --display=MODE Select display mode (classic, friendly, table, json)\n" + " -j Equivalent to --display=json\n" + " -s --service=SERVICE[:SERVICE…]\n" + " Query on 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" + "\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" + "\nSee the %s for details.\n" + , program_invocation_short_name + , link + ); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + + enum { + ARG_VERSION = 0x100, + ARG_NO_PAGER, + ARG_NO_LEGEND, + ARG_DISPLAY, + 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 }, + { "display", required_argument, NULL, ARG_DISPLAY }, + { "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_DISPLAY: + if (streq(optarg, "classic")) + arg_display = DISPLAY_CLASSIC; + else if (streq(optarg, "friendly")) + arg_display = DISPLAY_FRIENDLY; + else if (streq(optarg, "json")) + arg_display = DISPLAY_JSON; + else if (streq(optarg, "table")) + arg_display = DISPLAY_TABLE; + else if (streq(optarg, "help")) { + puts("classic\n" + "friendly\n" + "json"); + return 0; + } else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid --display= mode: %s", optarg); + + break; + + case 'j': + arg_display = DISPLAY_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 }, + {} + }; + + 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 0000000000000..a5f6b767238ee --- /dev/null +++ b/src/userdb/userdbd-manager.c @@ -0,0 +1,301 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "sd-daemon.h" + +#include "fd-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" +#include "fs-util.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, + }; + + 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; + + RATELIMIT_INIT(m->worker_ratelimit, 5 * USEC_PER_SEC, 50); + + *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); + } + + // FIXME drop this line + 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 0000000000000..0bf67fe55b443 --- /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 0000000000000..c7cf0e9485aeb --- /dev/null +++ b/src/userdb/userdbd.c @@ -0,0 +1,59 @@ +/* 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(); + + // FIXME + log_set_max_level(LOG_DEBUG); + + 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 0000000000000..8b9fbb5974640 --- /dev/null +++ b/src/userdb/userwork.c @@ -0,0 +1,785 @@ +/* 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 }, + {} + }; + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + 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(); + + // FIXME + log_set_max_level(LOG_DEBUG); + + 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); + + (void) rename_process("systemd-userwork: processing..."); + + if (fd < 0) { + if (errno == 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 (errno == EINTR) + continue; /* Might be that somebody attached via strace, let's just continue + * in that case */ + + return log_error_errno(errno, "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/test/TEST-36-HOMED/Makefile b/test/TEST-36-HOMED/Makefile new file mode 120000 index 0000000000000..e9f93b1104cd2 --- /dev/null +++ b/test/TEST-36-HOMED/Makefile @@ -0,0 +1 @@ +../TEST-01-BASIC/Makefile \ No newline at end of file diff --git a/test/TEST-36-HOMED/test.sh b/test/TEST-36-HOMED/test.sh new file mode 100755 index 0000000000000..8fee9e585616f --- /dev/null +++ b/test/TEST-36-HOMED/test.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -e +TEST_DESCRIPTION="testing homed" +TEST_NO_QEMU=1 + +. $TEST_BASE_DIR/test-functions + +test_setup() { + create_empty_image + mkdir -p $TESTDIR/root + mount ${LOOPDEV}p1 $TESTDIR/root + + ( + LOG_LEVEL=5 + eval $(udevadm info --export --query=env --name=${LOOPDEV}p2) + + setup_basic_environment + + # mask some services that we do not want to run in these tests + ln -fs /dev/null $initdir/etc/systemd/system/systemd-hwdb-update.service + ln -fs /dev/null $initdir/etc/systemd/system/systemd-journal-catalog-update.service + ln -fs /dev/null $initdir/etc/systemd/system/systemd-networkd.service + ln -fs /dev/null $initdir/etc/systemd/system/systemd-networkd.socket + ln -fs /dev/null $initdir/etc/systemd/system/systemd-resolved.service + ln -fs /dev/null $initdir/etc/systemd/system/systemd-machined.service + + # setup the testsuite service + cat >$initdir/etc/systemd/system/testsuite.service < /testok + +exit 0 diff --git a/units/meson.build b/units/meson.build index cf4fe2e7bfb80..2ced8abc7273e 100644 --- a/units/meson.build +++ b/units/meson.build @@ -94,6 +94,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', ''], @@ -180,6 +182,9 @@ in_units = [ ['systemd-nspawn@.service', ''], ['systemd-portabled.service', 'ENABLE_PORTABLED', 'dbus-org.freedesktop.portable1.service'], + ['systemd-userdbd.service', 'ENABLE_USERDB'], + ['systemd-homed.service', 'ENABLE_HOMED', + 'multi-user.target.wants/ dbus-org.freedesktop.home1.service'], ['systemd-quotacheck.service', 'ENABLE_QUOTACHECK'], ['systemd-random-seed.service', 'ENABLE_RANDOMSEED', 'sysinit.target.wants/'], diff --git a/units/systemd-homed.service.in b/units/systemd-homed.service.in new file mode 100644 index 0000000000000..cb5edcb30d8e6 --- /dev/null +++ b/units/systemd-homed.service.in @@ -0,0 +1,29 @@ +# 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=Home Manager +Documentation=man:systemd-homed.service(8) +RequiresMountsFor=/home + +[Service] +ExecStart=@rootlibexecdir@/systemd-homed +BusName=org.freedesktop.home1 +WatchdogSec=3min +#CapabilityBoundingSet=CAP_KILL CAP_SYS_PTRACE CAP_SYS_ADMIN CAP_SETGID CAP_SYS_CHROOT CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE CAP_CHOWN CAP_FOWNER CAP_FSETID CAP_MKNOD +MemoryDenyWriteExecute=yes +ProtectHostname=yes +RestrictRealtime=yes +RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6 AF_ALG +SystemCallFilter=@system-service @mount +SystemCallErrorNumber=EPERM +SystemCallArchitectures=native +LockPersonality=yes +IPAddressDeny=any +StateDirectory=systemd/home diff --git a/units/systemd-userdbd.service.in b/units/systemd-userdbd.service.in new file mode 100644 index 0000000000000..d451ec60243f5 --- /dev/null +++ b/units/systemd-userdbd.service.in @@ -0,0 +1,35 @@ +# 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] +ExecStart=@rootlibexecdir@/systemd-userdbd +Type=notify +WatchdogSec=3min +#CapabilityBoundingSet=CAP_KILL CAP_SYS_PTRACE CAP_SYS_ADMIN CAP_SETGID CAP_SYS_CHROOT CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE CAP_CHOWN CAP_FOWNER CAP_FSETID CAP_MKNOD +LockPersonality=yes +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes +ProtectHostname=yes +RestrictNamespaces=yes +RestrictRealtime=yes +RestrictSUIDSGID=yes +RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6 +SystemCallFilter=@system-service +SystemCallErrorNumber=EPERM +SystemCallArchitectures=native +IPAddressDeny=any +LimitNOFILE=@HIGH_RLIMIT_NOFILE@ diff --git a/units/systemd-userdbd.socket b/units/systemd-userdbd.socket new file mode 100644 index 0000000000000..1c749ea1d2321 --- /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