User/Group Record Lookup API via Varlink

JSON User/Group Records (as described in the JSON User Records and JSON Group Records documents) that are defined on the local system may be queried with a Varlink API. This API takes both the role of what getpwnam(3) and related calls are for struct passwd, as well as the interfaces modules implementing the glibc Name Service Switch (NSS) expose. Or in other words, it both allows applications to efficiently query user/group records from local services, and allows local subsystems to provide user/group records efficiently to local applications.

This simple API only exposes only three method calls, and requires only a small subset of the Varlink functionality.

The API described in this document is based on a simple subset of the mechanisms described by Varlink. The choice of preferring Varlink over D-Bus and other IPCs in this context was made for three reasons:

  1. User/Group record resolution should work during early boot and late shutdown without special handling. This is very hard to do with D-Bus, as the broker service for D-Bus generally runs as regular system daemon and is hence only available at the latest boot stage.

  2. The JSON user/group records are native JSON data, hence picking an IPC system that natively operates with JSON data is natural and clean.

  3. IPC systems such as D-Bus do not provide flow control and are thus unusable for streaming data. They are useful to pass around short control messages, but as soon as potentially many and large objects shall be transferred, D-Bus is not suitable, as any such streaming of messages would be considered flooding in D-Bus’ logic, and thus possibly result in termination of communication. Since the APIs defined in this document need to support enumerating potentially large numbers of users and groups, D-Bus is simply not an appropriate option.

Concepts

Each subsystem that needs to define users and groups on the local system is supposed to implement this API, and offer its interfaces on a Varlink AF_UNIX/SOCK_STREAM file system socket bound into the /run/systemd/userdb/ directory. When a client wants to look up a user or group record, it contacts all sockets bound in this directory in parallel, and enqueues the same query to each. The first positive reply is then returned to the application, or if all fail the last seen error is returned instead. (Alternatively a special Varlink service is available, io.systemd.Multiplexer which acts as frontend and will do the parallel queries on behalf of the client, drastically simplifying client development. This service is not available during earliest boot and final shutdown phases.)

Unlike with glibc NSS there’s no order or programmatic expression language defined in which queries are issued to the various services. Instead, all queries are always enqueued in parallel to all defined services, in order to make look-ups efficient, and the simple rule of “first successful lookup wins” is unconditionally followed for user and group look-ups (though not for membership lookups, see below).

This simple scheme only works safely as long as every service providing user/group records carefully makes sure not to answer with conflicting records. This API does not define any mechanisms for dealing with user/group name/ID collisions during look-up nor during record registration. It assumes the various subsystems that want to offer user and group records to the rest of the system have made sufficiently sure in advance that their definitions do not collide with those of other services. Clients are not expected to merge multiple definitions for the same user or group, and will also not be able to detect conflicts and suppress such conflicting records.

It is recommended to name the sockets in the directory in reverse domain name notation, but this is neither required nor enforced.

Well-Known Services

Any subsystem that wants to provide user/group records can do so, simply by binding a socket in the aforementioned directory. By default two services are listening there, that have special relevance:

  1. io.systemd.NameServiceSwitch → This service makes the classic UNIX/glibc NSS user/group records available as JSON User/Group records. Any such records are automatically converted as needed, and possibly augmented with information from the shadow databases.

  2. io.systemd.Multiplexer → This service multiplexes client queries to all other running services. It’s supposed to simplify client development: in order to look up or enumerate user/group records it’s sufficient to talk to one service instead of all of them in parallel. Note that it is not availabe during earliest boot and final shutdown phases, hence for programs running in that context it is preferable to implement the parallel lookup themselves.

Both these services are implemented by the same daemon systemd-userdbd.service.

Note that these services currently implement a subset of Varlink only. For example, introspection is not available, and the resolver logic is not used.

Other Services

The systemd project provides two other services implementing this interface. Specifically:

  1. io.systemd.DynamicUser → This service is implemented by the service manager itself, and provides records for the users and groups synthesized via DynamicUser= in unit files.

  2. io.systemd.Home → This service is implemented by systemd-homed.service and provides records for the users and groups defined by the home directories it manages.

Other projects are invited to implement these services too. For example it would make sense for LDAP/ActiveDirectory projects to implement these interfaces, which would provide them a way to do per-user resource management enforced by systemd and defined directly in LDAP directories.

Compatibility with NSS

Two-way compatibility with classic UNIX/glibc NSS user/group records is provided. When using the Varlink API, lookups into databases provided only via NSS (and not natively via Varlink) are handled by the io.systemd.NameServiceSwitch service (see above). When using the NSS API (i.e. getpwnam() and friends) the nss-systemd module will automatically synthesize NSS records for users/groups natively defined via a Varlink API. Special care is taken to avoid recursion between these two compatibility mechanisms.

Subsystems that shall provide user/group records to the system may choose between offering them via an NSS module or via a this Varlink API, either way all records are accessible via both APIs, due to the bidirectional forwarding. It is also possible to provide the same records via both APIs directly, but in that case the compatibility logic must be turned off. There are mechanisms in place for this, please contact the systemd project for details, as these are currently not documented.

Caching of User Records

This API defines no concepts for caching records. If caching is desired it should be implemented in the subsystems that provide the user records, not in the clients consuming them.

Method Calls

interface io.systemd.UserDatabase

method GetUserRecord(
        uid : ?int,
        userName : ?string,
        service : string
) -> (
        record : object,
        incomplete : boolean
)

method GetGroupRecord(
        gid : ?int,
        groupName : ?string,
        service : string
) -> (
        record : object,
        incomplete : boolean
)

method GetMemberships(
        userName : ?string,
        groupName : ?string,
        service : string
) -> (
        userName : string,
        groupName : string
)

error NoRecordFound()
error BadService()
error ServiceNotAvailable()
error ConflictingRecordFound()

The GetUserRecord method looks up or enumerates a user record. If the uid parameter is set it specifies the numeric UNIX UID to search for. If the userName parameter is set it specifies the name of the user to search for. Typically, only one of the two parameters are set, depending whether a look-up by UID or by name is desired. However, clients may also specify both parameters, in which case a record matching both will be returned, and if only one exists that matches one of the two parameters but not the other an error of ConflictingRecordFound is returned. If neither of the two parameters are set the whole user database is enumerated. In this case the method call needs to be made with more set, so that multiple method call replies may be generated as effect, each carrying one user record.

The service parameter is mandatory and should be set to the service name being talked to (i.e. to the same name as the AF_UNIX socket path, with the /run/systemd/userdb/ prefix removed). This is useful to allow implementation of multiple services on the same socket (which is used by systemd-userdbd.service).

The method call returns one or more user records, depending which type of query is used (see above). The record is returned in the record field. The incomplete field indicates whether the record is complete. Services providing user record lookup should only pass the privileged section of user records to clients that either match the user the record is about or to sufficiently privileged clients, for all others the section must be removed so that no sensitive data is leaked this way. The incomplete parameter should indicate whether the record has been modified like this or not (i.e. it is true if a privileged section existed in the user record and was removed, and false if no privileged section existed or one existed but hasn’t been removed).

If no user record matching the specified UID or name is known the error NoRecordFound is returned (this is also returned if neither UID nor name are specified, and hence enumeration requested but the subsystem currently has no users defined).

If a method call with an incorrectly set service field is received (i.e. either not set at all, or not to the service’s own name) a BadService error is generated. Finally, ServiceNotAvailable should be returned when the backing subsystem is not operational for some reason and hence no information about existence or non-existence of a record can be returned nor any user record at all. (The service field is defined in order to allow implementation of daemons that provide multiple distinct user/group services over the same AF_UNIX socket: in order to correctly determine which service a client wants to talk to the client needs to provide the name in each request.)

The GetGroupRecord method call works analogously but for groups.

The GetMemberships method call may be used to inquire about group memberships. The userName and groupName arguments take what the name suggests. If one of the two is specified all matching memberships are returned, if neither is specified all known memberships of any user and any group are returned. The return value is a pair of user name and group name, where the user is a member of the group. If both arguments are specified the specified membership will be tested for, but no others, and the pair is returned if it is defined. Unless both arguments are specified the method call needs to be made with more set, so that multiple replies can be returned (since typically there are are multiple members per group and also multiple groups a user is member of). As with GetUserRecord and GetGroupRecord the service parameter needs to contain the name of the service being talked to, in order to allow implementation of multiple service within the same IPC socket. In case no matching membership is known NoRecordFound is returned. The other two errors are also generated in the same cases as for GetUserRecord and GetGroupRecord.

Unlike with GetUserRecord and GetGroupRecord the lists of memberships returned by services are always combined. Thus unlike the other two calls a membership lookup query has to wait for the last simultaneous query to complete before the complete list is acquired.

Note that only the GetMemberships call is authoritative about memberships of users in groups. i.e. it should not be considered sufficient to check the memberOf field of user records and the members field of group records to acquire the full list of memberships. The full list can only bet determined by GetMemberships, and as mentioned requires merging of these lists of all local services. Result of this is that it can be one service that defines a user A, and another service that defines a group B, and a third service that declares that A is a member of B.

And that’s really all there is to it.