From 2ee4b118fa72d1dd119dc7b8915307ed86df5b14 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Aug 2019 12:33:33 +0200 Subject: [PATCH 01/29] nss-util: add macros for generating getpwent()/getgrent() prototypes We have similar macros already for getpwuid()/getpwnam(), let's add more of this. --- src/basic/nss-util.h | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/basic/nss-util.h b/src/basic/nss-util.h index 2045175d1c..29cf22676a 100644 --- a/src/basic/nss-util.h +++ b/src/basic/nss-util.h @@ -139,6 +139,38 @@ enum nss_status _nss_##module##_getgrgid_r( \ char *buffer, size_t buflen, \ int *errnop) _public_ +#define NSS_PWENT_PROTOTYPES(module) \ +enum nss_status _nss_##module##_endpwent( \ + void) _public_; \ +enum nss_status _nss_##module##_setpwent( \ + int stayopen) _public_; \ +enum nss_status _nss_##module##_getpwent_r( \ + struct passwd *result, \ + char *buffer, \ + size_t buflen, \ + int *errnop) _public_; + +#define NSS_GRENT_PROTOTYPES(module) \ +enum nss_status _nss_##module##_endgrent( \ + void) _public_; \ +enum nss_status _nss_##module##_setgrent( \ + int stayopen) _public_; \ +enum nss_status _nss_##module##_getgrent_r( \ + struct group *result, \ + char *buffer, \ + size_t buflen, \ + int *errnop) _public_; + +#define NSS_INITGROUPS_PROTOTYPE(module) \ +enum nss_status _nss_##module##_initgroups_dyn( \ + const char *user, \ + gid_t group, \ + long int *start, \ + long int *size, \ + gid_t **groupsp, \ + long int limit, \ + int *errnop) _public_; + typedef enum nss_status (*_nss_gethostbyname4_r_t)( const char *name, struct gaih_addrtuple **pat, From 42f3b2f97510ab364717154c01a03f5ddd9541a1 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 13 Nov 2019 23:13:42 +0100 Subject: [PATCH 02/29] shared: split out crypt() specific helpers into its own .c/.h in src/shared/ This way we can use libxcrypt specific functionality such as crypt_gensalt() and thus take benefit of the newer algorithms libxcrypt implements. (Also adds support for a new env var $SYSTEMD_CRYPT_PREFIX which may be used to select the hash algorithm to use for libxcrypt.) Also, let's move the weird crypt.h inclusion into libcrypt.h so that there's a single place for it. --- docs/ENVIRONMENT.md | 7 ++++ src/basic/user-util.c | 37 ------------------- src/basic/user-util.h | 2 - src/firstboot/firstboot.c | 14 +------ src/shared/libcrypt-util.c | 75 ++++++++++++++++++++++++++++++++++++++ src/shared/libcrypt-util.h | 20 ++++++++++ src/shared/meson.build | 3 ++ src/test/test-user-util.c | 1 + 8 files changed, 107 insertions(+), 52 deletions(-) create mode 100644 src/shared/libcrypt-util.c create mode 100644 src/shared/libcrypt-util.h diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 9b1df6b59c..fec40b1e9c 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -73,6 +73,13 @@ All tools: appropriate path under /run. This variable is also set by the manager when RuntimeDirectory= is used, see systemd.exec(5). +* `$SYSTEMD_CRYPT_PREFIX` — if set configures the hash method prefix to use for + UNIX crypt() when generating passwords. By default the system's "preferred + method" is used, but this can be overridden with this environment + variable. Takes a prefix such as `$6$` or `$y$`. (Note that this is only + honoured on systems built with libxcrypt and is ignored on systems using + glibc's original, internal crypt() implementation.) + systemctl: * `$SYSTEMCTL_FORCE_BUS=1` — if set, do not connect to PID1's private D-Bus diff --git a/src/basic/user-util.c b/src/basic/user-util.c index 571578c4e4..68c6dd0da7 100644 --- a/src/basic/user-util.c +++ b/src/basic/user-util.c @@ -945,40 +945,3 @@ int fgetsgent_sane(FILE *stream, struct sgrp **sg) { return !!s; } #endif - -int make_salt(char **ret) { - static const char table[] = - "abcdefghijklmnopqrstuvwxyz" - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "0123456789" - "./"; - - uint8_t raw[16]; - char *salt, *j; - size_t i; - int r; - - /* This is a bit like crypt_gensalt_ra(), but doesn't require libcrypt, and doesn't do anything but - * SHA512, i.e. is legacy-free and minimizes our deps. */ - - assert_cc(sizeof(table) == 64U + 1U); - - /* Insist on the best randomness by setting RANDOM_BLOCK, this is about keeping passwords secret after all. */ - r = genuine_random_bytes(raw, sizeof(raw), RANDOM_BLOCK); - if (r < 0) - return r; - - salt = new(char, 3+sizeof(raw)+1+1); - if (!salt) - return -ENOMEM; - - /* We only bother with SHA512 hashed passwords, the rest is legacy, and we don't do legacy. */ - j = stpcpy(salt, "$6$"); - for (i = 0; i < sizeof(raw); i++) - j[i] = table[raw[i] & 63]; - j[i++] = '$'; - j[i] = 0; - - *ret = salt; - return 0; -} diff --git a/src/basic/user-util.h b/src/basic/user-util.h index e11dd9b379..714e83082c 100644 --- a/src/basic/user-util.h +++ b/src/basic/user-util.h @@ -137,6 +137,4 @@ int fgetsgent_sane(FILE *stream, struct sgrp **sg); int putsgent_sane(const struct sgrp *sg, FILE *stream); #endif -int make_salt(char **ret); - bool is_nologin_shell(const char *shell); diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index 528e6452cf..8001f90d57 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -4,19 +4,6 @@ #include #include -#if HAVE_CRYPT_H -/* libxcrypt is a replacement for glibc's libcrypt, and libcrypt might be - * removed from glibc at some point. As part of the removal, defines for - * crypt(3) are dropped from unistd.h, and we must include crypt.h instead. - * - * Newer versions of glibc (v2.0+) already ship crypt.h with a definition - * of crypt(3) as well, so we simply include it if it is present. MariaDB, - * MySQL, PostgreSQL, Perl and some other wide-spread packages do it the - * same way since ages without any problems. - */ -# include -#endif - #include "sd-id128.h" #include "alloc-util.h" @@ -28,6 +15,7 @@ #include "fs-util.h" #include "hostname-util.h" #include "kbd-util.h" +#include "libcrypt-util.h" #include "locale-util.h" #include "main-func.h" #include "memory-util.h" diff --git a/src/shared/libcrypt-util.c b/src/shared/libcrypt-util.c new file mode 100644 index 0000000000..b1a8168030 --- /dev/null +++ b/src/shared/libcrypt-util.c @@ -0,0 +1,75 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include + +#include "alloc-util.h" +#include "libcrypt-util.h" +#include "log.h" +#include "macro.h" +#include "missing_stdlib.h" +#include "random-util.h" +#include "string-util.h" +#include "strv.h" + +int make_salt(char **ret) { + +#ifdef XCRYPT_VERSION_MAJOR + const char *e; + char *salt; + + /* If we have libxcrypt we default to the "preferred method" (i.e. usually yescrypt), and generate it + * with crypt_gensalt_ra(). */ + + e = secure_getenv("SYSTEMD_CRYPT_PREFIX"); + if (!e) + e = crypt_preferred_method(); + + log_debug("Generating salt for hash prefix: %s", e); + + salt = crypt_gensalt_ra(e, 0, NULL, 0); + if (!salt) + return -errno; + + *ret = salt; + return 0; +#else + /* If libxcrypt is not used, we use SHA512 and generate the salt on our own since crypt_gensalt_ra() + * is not available. */ + + static const char table[] = + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789" + "./"; + + uint8_t raw[16]; + char *salt, *j; + size_t i; + int r; + + /* This is a bit like crypt_gensalt_ra(), but doesn't require libcrypt, and doesn't do anything but + * SHA512, i.e. is legacy-free and minimizes our deps. */ + + assert_cc(sizeof(table) == 64U + 1U); + + /* Insist on the best randomness by setting RANDOM_BLOCK, this is about keeping passwords secret after all. */ + r = genuine_random_bytes(raw, sizeof(raw), RANDOM_BLOCK); + if (r < 0) + return r; + + salt = new(char, 3+sizeof(raw)+1+1); + if (!salt) + return -ENOMEM; + + /* We only bother with SHA512 hashed passwords, the rest is legacy, and we don't do legacy. */ + j = stpcpy(salt, "$6$"); + for (i = 0; i < sizeof(raw); i++) + j[i] = table[raw[i] & 63]; + j[i++] = '$'; + j[i] = 0; + + *ret = salt; + return 0; +#endif +} diff --git a/src/shared/libcrypt-util.h b/src/shared/libcrypt-util.h new file mode 100644 index 0000000000..3da1ab5ad9 --- /dev/null +++ b/src/shared/libcrypt-util.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#if HAVE_CRYPT_H +/* libxcrypt is a replacement for glibc's libcrypt, and libcrypt might be + * removed from glibc at some point. As part of the removal, defines for + * crypt(3) are dropped from unistd.h, and we must include crypt.h instead. + * + * Newer versions of glibc (v2.0+) already ship crypt.h with a definition + * of crypt(3) as well, so we simply include it if it is present. MariaDB, + * MySQL, PostgreSQL, Perl and some other wide-spread packages do it the + * same way since ages without any problems. + */ +#include +#endif + +#include +#include + +int make_salt(char **ret); diff --git a/src/shared/meson.build b/src/shared/meson.build index 08aa480b6c..f4f5ebf3c4 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -106,6 +106,8 @@ shared_sources = files(''' json-internal.h json.c json.h + libcrypt-util.c + libcrypt-util.h libmount-util.h linux/auto_dev-ioctl.h linux/bpf.h @@ -279,6 +281,7 @@ libshared_deps = [threads, libacl, libblkid, libcap, + libcrypt, libcryptsetup, libgcrypt, libidn, diff --git a/src/test/test-user-util.c b/src/test/test-user-util.c index fde8336bf9..6bfe7dc7e1 100644 --- a/src/test/test-user-util.c +++ b/src/test/test-user-util.c @@ -2,6 +2,7 @@ #include "alloc-util.h" #include "format-util.h" +#include "libcrypt-util.h" #include "log.h" #include "macro.h" #include "memory-util.h" From 64aa2622a3baea1f62732f136230543c9d669cd1 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 13 Nov 2019 23:28:05 +0100 Subject: [PATCH 03/29] libcrypt-util: add superficial validator for UNIX hashed password strings --- src/shared/libcrypt-util.c | 11 +++++++++++ src/shared/libcrypt-util.h | 2 ++ 2 files changed, 13 insertions(+) diff --git a/src/shared/libcrypt-util.c b/src/shared/libcrypt-util.c index b1a8168030..f41685ae45 100644 --- a/src/shared/libcrypt-util.c +++ b/src/shared/libcrypt-util.c @@ -73,3 +73,14 @@ int make_salt(char **ret) { return 0; #endif } + +bool hashed_password_valid(const char *s) { + + /* Returns true if the specified string is a 'valid' hashed UNIX password, i.e. if starts with '$' or + * with '!$' (the latter being a valid, yet locked password). */ + + if (isempty(s)) + return false; + + return STARTSWITH_SET(s, "$", "!$"); +} diff --git a/src/shared/libcrypt-util.h b/src/shared/libcrypt-util.h index 3da1ab5ad9..93f0e13ffb 100644 --- a/src/shared/libcrypt-util.h +++ b/src/shared/libcrypt-util.h @@ -18,3 +18,5 @@ #include int make_salt(char **ret); + +bool hashed_password_valid(const char *s); From 71d0b9d42263272cb05a7587404e6f2d9006bdb2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Aug 2019 15:25:36 +0200 Subject: [PATCH 04/29] shared: add generic user/group record structures and JSON parsers --- src/shared/group-record.c | 346 +++++++ src/shared/group-record.h | 44 + src/shared/meson.build | 4 + src/shared/user-record.c | 1883 +++++++++++++++++++++++++++++++++++++ src/shared/user-record.h | 375 ++++++++ 5 files changed, 2652 insertions(+) create mode 100644 src/shared/group-record.c create mode 100644 src/shared/group-record.h create mode 100644 src/shared/user-record.c create mode 100644 src/shared/user-record.h diff --git a/src/shared/group-record.c b/src/shared/group-record.c new file mode 100644 index 0000000000..d5ec32f07d --- /dev/null +++ b/src/shared/group-record.c @@ -0,0 +1,346 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "group-record.h" +#include "strv.h" +#include "user-util.h" + +GroupRecord* group_record_new(void) { + GroupRecord *h; + + h = new(GroupRecord, 1); + if (!h) + return NULL; + + *h = (GroupRecord) { + .n_ref = 1, + .disposition = _USER_DISPOSITION_INVALID, + .last_change_usec = UINT64_MAX, + .gid = GID_INVALID, + }; + + return h; +} + +static GroupRecord *group_record_free(GroupRecord *g) { + if (!g) + return NULL; + + free(g->group_name); + free(g->realm); + free(g->group_name_and_realm_auto); + + strv_free(g->members); + free(g->service); + strv_free(g->administrators); + strv_free_erase(g->hashed_password); + + json_variant_unref(g->json); + + return mfree(g); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(GroupRecord, group_record, group_record_free); + +static int dispatch_privileged(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + + static const JsonDispatch privileged_dispatch_table[] = { + { "hashedPassword", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(GroupRecord, hashed_password), JSON_SAFE }, + {}, + }; + + return json_dispatch(variant, privileged_dispatch_table, NULL, flags, userdata); +} + +static int dispatch_binding(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + + static const JsonDispatch binding_dispatch_table[] = { + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(GroupRecord, gid), 0 }, + {}, + }; + + char smid[SD_ID128_STRING_MAX]; + JsonVariant *m; + sd_id128_t mid; + int r; + + if (!variant) + return 0; + + if (!json_variant_is_object(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name)); + + r = sd_id128_get_machine(&mid); + if (r < 0) + return json_log(variant, flags, r, "Failed to determine machine ID: %m"); + + m = json_variant_by_key(variant, sd_id128_to_string(mid, smid)); + if (!m) + return 0; + + return json_dispatch(m, binding_dispatch_table, NULL, flags, userdata); +} + +static int dispatch_per_machine(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + + static const JsonDispatch per_machine_dispatch_table[] = { + { "matchMachineId", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, + { "matchHostname", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(GroupRecord, gid), 0 }, + { "members", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, members), 0 }, + { "administrators", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, administrators), 0 }, + {}, + }; + + JsonVariant *e; + int r; + + if (!variant) + return 0; + + if (!json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name)); + + JSON_VARIANT_ARRAY_FOREACH(e, variant) { + bool matching = false; + JsonVariant *m; + + if (!json_variant_is_object(e)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of objects.", strna(name)); + + m = json_variant_by_key(e, "matchMachineId"); + if (m) { + r = per_machine_id_match(m, flags); + if (r < 0) + return r; + + matching = r > 0; + } + + if (!matching) { + m = json_variant_by_key(e, "matchHostname"); + if (m) { + r = per_machine_hostname_match(m, flags); + if (r < 0) + return r; + + matching = r > 0; + } + } + + if (!matching) + continue; + + r = json_dispatch(e, per_machine_dispatch_table, NULL, flags, userdata); + if (r < 0) + return r; + } + + return 0; +} + +static int dispatch_status(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + + static const JsonDispatch status_dispatch_table[] = { + { "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(GroupRecord, service), JSON_SAFE }, + {}, + }; + + char smid[SD_ID128_STRING_MAX]; + JsonVariant *m; + sd_id128_t mid; + int r; + + if (!variant) + return 0; + + if (!json_variant_is_object(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name)); + + r = sd_id128_get_machine(&mid); + if (r < 0) + return json_log(variant, flags, r, "Failed to determine machine ID: %m"); + + m = json_variant_by_key(variant, sd_id128_to_string(mid, smid)); + if (!m) + return 0; + + return json_dispatch(m, status_dispatch_table, NULL, flags, userdata); +} + +static int group_record_augment(GroupRecord *h, JsonDispatchFlags json_flags) { + assert(h); + + if (!FLAGS_SET(h->mask, USER_RECORD_REGULAR)) + return 0; + + assert(h->group_name); + + if (!h->group_name_and_realm_auto && h->realm) { + h->group_name_and_realm_auto = strjoin(h->group_name, "@", h->realm); + if (!h->group_name_and_realm_auto) + return json_log_oom(h->json, json_flags); + } + + return 0; +} + +int group_record_load( + GroupRecord *h, + JsonVariant *v, + UserRecordLoadFlags load_flags) { + + static const JsonDispatch group_dispatch_table[] = { + { "groupName", JSON_VARIANT_STRING, json_dispatch_user_group_name, offsetof(GroupRecord, group_name), 0 }, + { "realm", JSON_VARIANT_STRING, json_dispatch_realm, offsetof(GroupRecord, realm), 0 }, + { "disposition", JSON_VARIANT_STRING, json_dispatch_user_disposition, offsetof(GroupRecord, disposition), 0 }, + { "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(GroupRecord, service), JSON_SAFE }, + { "lastChangeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(GroupRecord, last_change_usec), 0 }, + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(GroupRecord, gid), 0 }, + { "members", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, members), 0 }, + { "administrators", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, administrators), 0 }, + + { "privileged", JSON_VARIANT_OBJECT, dispatch_privileged, 0, 0 }, + + /* Not defined for now, for groups, but let's at least generate sensible errors about it */ + { "secret", JSON_VARIANT_OBJECT, json_dispatch_unsupported, 0, 0 }, + + /* Ignore the perMachine, binding and status stuff here, and process it later, so that it overrides whatever is set above */ + { "perMachine", JSON_VARIANT_ARRAY, NULL, 0, 0 }, + { "binding", JSON_VARIANT_OBJECT, NULL, 0, 0 }, + { "status", JSON_VARIANT_OBJECT, NULL, 0, 0 }, + + /* Ignore 'signature', we check it with explicit accessors instead */ + { "signature", JSON_VARIANT_ARRAY, NULL, 0, 0 }, + {}, + }; + + JsonDispatchFlags json_flags = USER_RECORD_LOAD_FLAGS_TO_JSON_DISPATCH_FLAGS(load_flags); + int r; + + assert(h); + assert(!h->json); + + /* Note that this call will leave a half-initialized record around on failure! */ + + if ((USER_RECORD_REQUIRE_MASK(load_flags) & (USER_RECORD_SECRET|USER_RECORD_PRIVILEGED))) + return json_log(v, json_flags, SYNTHETIC_ERRNO(EINVAL), "Secret and privileged section currently not available for groups, refusing."); + + r = user_group_record_mangle(v, load_flags, &h->json, &h->mask); + if (r < 0) + return r; + + r = json_dispatch(h->json, group_dispatch_table, NULL, json_flags, h); + if (r < 0) + return r; + + /* During the parsing operation above we ignored the 'perMachine', 'binding' and 'status' fields, since we want + * them to override the global options. Let's process them now. */ + + r = dispatch_per_machine("perMachine", json_variant_by_key(h->json, "perMachine"), json_flags, h); + if (r < 0) + return r; + + r = dispatch_binding("binding", json_variant_by_key(h->json, "binding"), json_flags, h); + if (r < 0) + return r; + + r = dispatch_status("status", json_variant_by_key(h->json, "status"), json_flags, h); + if (r < 0) + return r; + + if (FLAGS_SET(h->mask, USER_RECORD_REGULAR) && !h->group_name) + return json_log(h->json, json_flags, SYNTHETIC_ERRNO(EINVAL), "Group name field missing, refusing."); + + r = group_record_augment(h, json_flags); + if (r < 0) + return r; + + return 0; +} + +int group_record_build(GroupRecord **ret, ...) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; + va_list ap; + int r; + + assert(ret); + + va_start(ap, ret); + r = json_buildv(&v, ap); + va_end(ap); + + if (r < 0) + return r; + + g = group_record_new(); + if (!g) + return -ENOMEM; + + r = group_record_load(g, v, USER_RECORD_LOAD_FULL); + if (r < 0) + return r; + + *ret = TAKE_PTR(g); + return 0; +} + +const char *group_record_group_name_and_realm(GroupRecord *h) { + assert(h); + + /* Return the pre-initialized joined string if it is defined */ + if (h->group_name_and_realm_auto) + return h->group_name_and_realm_auto; + + /* If it's not defined then we cannot have a realm */ + assert(!h->realm); + return h->group_name; +} + +UserDisposition group_record_disposition(GroupRecord *h) { + assert(h); + + if (h->disposition >= 0) + return h->disposition; + + /* If not declared, derive from GID */ + + if (!gid_is_valid(h->gid)) + return _USER_DISPOSITION_INVALID; + + if (h->gid == 0 || h->gid == GID_NOBODY) + return USER_INTRINSIC; + + if (gid_is_system(h->gid)) + return USER_SYSTEM; + + if (gid_is_dynamic(h->gid)) + return USER_DYNAMIC; + + if (gid_is_container(h->gid)) + return USER_CONTAINER; + + if (h->gid > INT32_MAX) + return USER_RESERVED; + + return USER_REGULAR; +} + +int group_record_clone(GroupRecord *h, UserRecordLoadFlags flags, GroupRecord **ret) { + _cleanup_(group_record_unrefp) GroupRecord *c = NULL; + int r; + + assert(h); + assert(ret); + + c = group_record_new(); + if (!c) + return -ENOMEM; + + r = group_record_load(c, h->json, flags); + if (r < 0) + return r; + + *ret = TAKE_PTR(c); + return 0; +} diff --git a/src/shared/group-record.h b/src/shared/group-record.h new file mode 100644 index 0000000000..b72a43e50d --- /dev/null +++ b/src/shared/group-record.h @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "json.h" +#include "user-record.h" + +typedef struct GroupRecord { + unsigned n_ref; + UserRecordMask mask; + bool incomplete; + + char *group_name; + char *realm; + char *group_name_and_realm_auto; + + UserDisposition disposition; + uint64_t last_change_usec; + + gid_t gid; + + char **members; + + char *service; + + /* The following exist mostly so that we can cover the full /etc/gshadow set of fields, we currently + * do not actually make use of these */ + char **administrators; /* maps to 'struct sgrp' .sg_adm field */ + char **hashed_password; /* maps to 'struct sgrp' .sg_passwd field */ + + JsonVariant *json; +} GroupRecord; + +GroupRecord* group_record_new(void); +GroupRecord* group_record_ref(GroupRecord *g); +GroupRecord* group_record_unref(GroupRecord *g); + +DEFINE_TRIVIAL_CLEANUP_FUNC(GroupRecord*, group_record_unref); + +int group_record_load(GroupRecord *h, JsonVariant *v, UserRecordLoadFlags flags); +int group_record_build(GroupRecord **ret, ...); +int group_record_clone(GroupRecord *g, UserRecordLoadFlags flags, GroupRecord **ret); + +const char *group_record_group_name_and_realm(GroupRecord *h); +UserDisposition group_record_disposition(GroupRecord *h); diff --git a/src/shared/meson.build b/src/shared/meson.build index f4f5ebf3c4..f9799fee1d 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -86,6 +86,8 @@ shared_sources = files(''' generator.c generator.h gpt.h + group-record.c + group-record.h id128-print.c id128-print.h ima-util.c @@ -189,6 +191,8 @@ shared_sources = files(''' uid-range.h unit-file.c unit-file.h + user-record.c + user-record.h utmp-wtmp.h varlink.c varlink.h diff --git a/src/shared/user-record.c b/src/shared/user-record.c new file mode 100644 index 0000000000..327cf8da87 --- /dev/null +++ b/src/shared/user-record.c @@ -0,0 +1,1883 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "cgroup-util.h" +#include "dns-domain.h" +#include "env-util.h" +#include "fs-util.h" +#include "hexdecoct.h" +#include "hostname-util.h" +#include "memory-util.h" +#include "path-util.h" +#include "pkcs11-util.h" +#include "rlimit-util.h" +#include "stat-util.h" +#include "string-table.h" +#include "strv.h" +#include "user-record.h" +#include "user-util.h" + +#define DEFAULT_RATELIMIT_BURST 30 +#define DEFAULT_RATELIMIT_INTERVAL_USEC (1*USEC_PER_MINUTE) + +UserRecord* user_record_new(void) { + UserRecord *h; + + h = new(UserRecord, 1); + if (!h) + return NULL; + + *h = (UserRecord) { + .n_ref = 1, + .disposition = _USER_DISPOSITION_INVALID, + .last_change_usec = UINT64_MAX, + .last_password_change_usec = UINT64_MAX, + .umask = MODE_INVALID, + .nice_level = INT_MAX, + .not_before_usec = UINT64_MAX, + .not_after_usec = UINT64_MAX, + .locked = -1, + .storage = _USER_STORAGE_INVALID, + .access_mode = MODE_INVALID, + .disk_size = UINT64_MAX, + .disk_size_relative = UINT64_MAX, + .tasks_max = UINT64_MAX, + .memory_high = UINT64_MAX, + .memory_max = UINT64_MAX, + .cpu_weight = UINT64_MAX, + .io_weight = UINT64_MAX, + .uid = UID_INVALID, + .gid = GID_INVALID, + .nodev = true, + .nosuid = true, + .luks_discard = -1, + .luks_volume_key_size = UINT64_MAX, + .luks_pbkdf_time_cost_usec = UINT64_MAX, + .luks_pbkdf_memory_cost = UINT64_MAX, + .luks_pbkdf_parallel_threads = UINT64_MAX, + .disk_usage = UINT64_MAX, + .disk_free = UINT64_MAX, + .disk_ceiling = UINT64_MAX, + .disk_floor = UINT64_MAX, + .signed_locally = -1, + .good_authentication_counter = UINT64_MAX, + .bad_authentication_counter = UINT64_MAX, + .last_good_authentication_usec = UINT64_MAX, + .last_bad_authentication_usec = UINT64_MAX, + .ratelimit_begin_usec = UINT64_MAX, + .ratelimit_count = UINT64_MAX, + .ratelimit_interval_usec = UINT64_MAX, + .ratelimit_burst = UINT64_MAX, + .removable = -1, + .enforce_password_policy = -1, + .auto_login = -1, + .stop_delay_usec = UINT64_MAX, + .kill_processes = -1, + .password_change_min_usec = UINT64_MAX, + .password_change_max_usec = UINT64_MAX, + .password_change_warn_usec = UINT64_MAX, + .password_change_inactive_usec = UINT64_MAX, + .password_change_now = -1, + .pkcs11_protected_authentication_path_permitted = -1, + }; + + return h; +} + +static void pkcs11_encrypted_key_done(Pkcs11EncryptedKey *k) { + if (!k) + return; + + free(k->uri); + erase_and_free(k->data); + erase_and_free(k->hashed_password); +} + +static UserRecord* user_record_free(UserRecord *h) { + if (!h) + return NULL; + + free(h->user_name); + free(h->realm); + free(h->user_name_and_realm_auto); + free(h->real_name); + free(h->email_address); + erase_and_free(h->password_hint); + free(h->location); + free(h->icon_name); + + free(h->shell); + + strv_free(h->environment); + free(h->time_zone); + free(h->preferred_language); + rlimit_free_all(h->rlimits); + + free(h->skeleton_directory); + + strv_free_erase(h->hashed_password); + strv_free_erase(h->ssh_authorized_keys); + strv_free_erase(h->password); + strv_free_erase(h->pkcs11_pin); + + free(h->cifs_service); + free(h->cifs_user_name); + free(h->cifs_domain); + + free(h->image_path); + free(h->image_path_auto); + free(h->home_directory); + free(h->home_directory_auto); + + strv_free(h->member_of); + + free(h->file_system_type); + free(h->luks_cipher); + free(h->luks_cipher_mode); + free(h->luks_pbkdf_hash_algorithm); + free(h->luks_pbkdf_type); + + free(h->state); + free(h->service); + + strv_free(h->pkcs11_token_uri); + for (size_t i = 0; i < h->n_pkcs11_encrypted_key; i++) + pkcs11_encrypted_key_done(h->pkcs11_encrypted_key + i); + free(h->pkcs11_encrypted_key); + + json_variant_unref(h->json); + + return mfree(h); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(UserRecord, user_record, user_record_free); + +int json_dispatch_realm(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + char **s = userdata; + const char *n; + int r; + + if (json_variant_is_null(variant)) { + *s = mfree(*s); + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + n = json_variant_string(variant); + r = dns_name_is_valid(n); + if (r < 0) + return json_log(variant, flags, r, "Failed to check if JSON field '%s' is a valid DNS domain.", strna(name)); + if (r == 0) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid DNS domain.", strna(name)); + + r = free_and_strdup(s, n); + if (r < 0) + return json_log(variant, flags, r, "Failed to allocate string: %m"); + + return 0; +} + +static int json_dispatch_gecos(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + char **s = userdata; + const char *n; + int r; + + if (json_variant_is_null(variant)) { + *s = mfree(*s); + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + n = json_variant_string(variant); + if (!valid_gecos(n)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid GECOS compatible real name.", strna(name)); + + r = free_and_strdup(s, n); + if (r < 0) + return json_log(variant, flags, r, "Failed to allocate string: %m"); + + return 0; +} + +static int json_dispatch_nice(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + int *nl = userdata; + intmax_t m; + + if (json_variant_is_null(variant)) { + *nl = INT_MAX; + return 0; + } + + if (!json_variant_is_integer(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + m = json_variant_integer(variant); + if (m < PRIO_MIN || m >= PRIO_MAX) + return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "JSON field '%s' is not a valid nice level.", strna(name)); + + *nl = m; + return 0; +} + +static int json_dispatch_rlimit_value(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + rlim_t *ret = userdata; + + if (json_variant_is_null(variant)) + *ret = RLIM_INFINITY; + else if (json_variant_is_unsigned(variant)) { + uintmax_t w; + + w = json_variant_unsigned(variant); + if (w == RLIM_INFINITY || (uintmax_t) w != json_variant_unsigned(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "Resource limit value '%s' is out of range.", name); + + *ret = (rlim_t) w; + } else + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit value '%s' is not an unsigned integer.", name); + + return 0; +} + +static int json_dispatch_rlimits(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + struct rlimit** limits = userdata; + JsonVariant *value; + const char *key; + int r; + + assert_se(limits); + + if (json_variant_is_null(variant)) { + rlimit_free_all(limits); + return 0; + } + + if (!json_variant_is_object(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name)); + + JSON_VARIANT_OBJECT_FOREACH(key, value, variant) { + JsonVariant *jcur, *jmax; + struct rlimit rl; + const char *p; + int l; + + p = startswith(key, "RLIMIT_"); + if (!p) + l = -1; + else + l = rlimit_from_string(p); + if (l < 0) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' not known.", key); + + if (!json_variant_is_object(value)) + return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' has invalid value.", key); + + if (json_variant_elements(value) != 4) + return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' value is does not have two fields as expected.", key); + + jcur = json_variant_by_key(value, "cur"); + if (!jcur) + return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' lacks 'cur' field.", key); + r = json_dispatch_rlimit_value("cur", jcur, flags, &rl.rlim_cur); + if (r < 0) + return r; + + jmax = json_variant_by_key(value, "max"); + if (!jmax) + return json_log(value, flags, SYNTHETIC_ERRNO(EINVAL), "Resource limit '%s' lacks 'max' field.", key); + r = json_dispatch_rlimit_value("max", jmax, flags, &rl.rlim_max); + if (r < 0) + return r; + + if (limits[l]) + *(limits[l]) = rl; + else { + limits[l] = newdup(struct rlimit, &rl, 1); + if (!limits[l]) + return log_oom(); + } + } + + return 0; +} + +static int json_dispatch_filename_or_path(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + char **s = userdata; + const char *n; + int r; + + assert(s); + + if (json_variant_is_null(variant)) { + *s = mfree(*s); + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + n = json_variant_string(variant); + if (!filename_is_valid(n) && !path_is_normalized(n)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid file name or normalized path.", strna(name)); + + r = free_and_strdup(s, n); + if (r < 0) + return json_log(variant, flags, r, "Failed to allocate string: %m"); + + return 0; +} + +static int json_dispatch_path(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + char **s = userdata; + const char *n; + int r; + + if (json_variant_is_null(variant)) { + *s = mfree(*s); + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + n = json_variant_string(variant); + if (!path_is_normalized(n)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a normalized file system path.", strna(name)); + if (!path_is_absolute(n)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an absolute file system path.", strna(name)); + + r = free_and_strdup(s, n); + if (r < 0) + return json_log(variant, flags, r, "Failed to allocate string: %m"); + + return 0; +} + +static int json_dispatch_home_directory(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + char **s = userdata; + const char *n; + int r; + + if (json_variant_is_null(variant)) { + *s = mfree(*s); + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + n = json_variant_string(variant); + if (!valid_home(n)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid home directory path.", strna(name)); + + r = free_and_strdup(s, n); + if (r < 0) + return json_log(variant, flags, r, "Failed to allocate string: %m"); + + return 0; +} + +static int json_dispatch_image_path(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + char **s = userdata; + const char *n; + int r; + + if (json_variant_is_null(variant)) { + *s = mfree(*s); + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + n = json_variant_string(variant); + if (empty_or_root(n) || !path_is_valid(n) || !path_is_absolute(n)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid image path.", strna(name)); + + r = free_and_strdup(s, n); + if (r < 0) + return json_log(variant, flags, r, "Failed to allocate string: %m"); + + return 0; +} + +static int json_dispatch_umask(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + mode_t *m = userdata; + uintmax_t k; + + if (json_variant_is_null(variant)) { + *m = (mode_t) -1; + return 0; + } + + if (!json_variant_is_unsigned(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a number.", strna(name)); + + k = json_variant_unsigned(variant); + if (k > 0777) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' outside of valid range 0…0777.", strna(name)); + + *m = (mode_t) k; + return 0; +} + +static int json_dispatch_access_mode(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + mode_t *m = userdata; + uintmax_t k; + + if (json_variant_is_null(variant)) { + *m = (mode_t) -1; + return 0; + } + + if (!json_variant_is_unsigned(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a number.", strna(name)); + + k = json_variant_unsigned(variant); + if (k > 07777) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' outside of valid range 0…07777.", strna(name)); + + *m = (mode_t) k; + return 0; +} + +static int json_dispatch_environment(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + _cleanup_strv_free_ char **n = NULL; + char ***l = userdata; + size_t i; + int r; + + if (json_variant_is_null(variant)) { + *l = strv_free(*l); + return 0; + } + + if (!json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name)); + + for (i = 0; i < json_variant_elements(variant); i++) { + _cleanup_free_ char *c = NULL; + JsonVariant *e; + const char *a; + + e = json_variant_by_index(variant, i); + if (!json_variant_is_string(e)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of strings.", strna(name)); + + assert_se(a = json_variant_string(e)); + + if (!env_assignment_is_valid(a)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of environment variables.", strna(name)); + + c = strdup(a); + if (!c) + return json_log_oom(variant, flags); + + r = strv_env_replace(&n, c); + if (r < 0) + return json_log_oom(variant, flags); + + c = NULL; + } + + strv_free_and_replace(*l, n); + return 0; +} + +int json_dispatch_user_disposition(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + UserDisposition *disposition = userdata, k; + + if (json_variant_is_null(variant)) { + *disposition = _USER_DISPOSITION_INVALID; + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + k = user_disposition_from_string(json_variant_string(variant)); + if (k < 0) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Disposition type '%s' not known.", json_variant_string(variant)); + + *disposition = k; + return 0; +} + +static int json_dispatch_storage(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + UserStorage *storage = userdata, k; + + if (json_variant_is_null(variant)) { + *storage = _USER_STORAGE_INVALID; + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + k = user_storage_from_string(json_variant_string(variant)); + if (k < 0) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Storage type '%s' not known.", json_variant_string(variant)); + + *storage = k; + return 0; +} + +static int json_dispatch_disk_size(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + uint64_t *size = userdata; + uintmax_t k; + + if (json_variant_is_null(variant)) { + *size = UINT64_MAX; + return 0; + } + + if (!json_variant_is_unsigned(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an integer.", strna(name)); + + k = json_variant_unsigned(variant); + if (k < USER_DISK_SIZE_MIN || k > USER_DISK_SIZE_MAX) + return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "JSON field '%s' is not in valid range %" PRIu64 "…%" PRIu64 ".", strna(name), USER_DISK_SIZE_MIN, USER_DISK_SIZE_MAX); + + *size = k; + return 0; +} + +static int json_dispatch_tasks_or_memory_max(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + uint64_t *limit = userdata; + uintmax_t k; + + if (json_variant_is_null(variant)) { + *limit = UINT64_MAX; + return 0; + } + + if (!json_variant_is_unsigned(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a integer.", strna(name)); + + k = json_variant_unsigned(variant); + if (k <= 0 || k >= UINT64_MAX) + return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "JSON field '%s' is not in valid range %" PRIu64 "…%" PRIu64 ".", strna(name), (uint64_t) 1, UINT64_MAX-1); + + *limit = k; + return 0; +} + +static int json_dispatch_weight(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + uint64_t *weight = userdata; + uintmax_t k; + + if (json_variant_is_null(variant)) { + *weight = UINT64_MAX; + return 0; + } + + if (!json_variant_is_unsigned(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a integer.", strna(name)); + + k = json_variant_unsigned(variant); + if (k <= CGROUP_WEIGHT_MIN || k >= CGROUP_WEIGHT_MAX) + return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "JSON field '%s' is not in valid range %" PRIu64 "…%" PRIu64 ".", strna(name), (uint64_t) CGROUP_WEIGHT_MIN, (uint64_t) CGROUP_WEIGHT_MAX); + + *weight = k; + return 0; +} + +int json_dispatch_user_group_list(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + _cleanup_strv_free_ char **l = NULL; + char ***list = userdata; + JsonVariant *e; + int r; + + if (!json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of strings.", strna(name)); + + JSON_VARIANT_ARRAY_FOREACH(e, variant) { + + if (!json_variant_is_string(e)) + return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not a string."); + + if (!valid_user_group_name(json_variant_string(e))) + return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not a valid user/group name: %s", json_variant_string(e)); + + r = strv_extend(&l, json_variant_string(e)); + if (r < 0) + return json_log(e, flags, r, "Failed to append array element: %m"); + } + + r = strv_extend_strv(list, l, true); + if (r < 0) + return json_log(variant, flags, r, "Failed to merge user/group arrays: %m"); + + return 0; +} + +static int dispatch_secret(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + + static const JsonDispatch secret_dispatch_table[] = { + { "password", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, password), 0 }, + { "pkcs11Pin", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, pkcs11_pin), 0 }, + { "pkcs11ProtectedAuthenticationPathPermitted", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, pkcs11_protected_authentication_path_permitted), 0 }, + {}, + }; + + return json_dispatch(variant, secret_dispatch_table, NULL, flags, userdata); +} + +static int dispatch_pkcs11_uri(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + char **s = userdata; + const char *n; + int r; + + if (json_variant_is_null(variant)) { + *s = mfree(*s); + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + n = json_variant_string(variant); + if (!pkcs11_uri_valid(n)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid RFC7512 PKCS#11 URI.", strna(name)); + + r = free_and_strdup(s, n); + if (r < 0) + return json_log(variant, flags, r, "Failed to allocate string: %m"); + + return 0; +} + +static int dispatch_pkcs11_uri_array(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + _cleanup_strv_free_ char **z = NULL; + char ***l = userdata; + JsonVariant *e; + int r; + + if (json_variant_is_null(variant)) { + *l = strv_free(*l); + return 0; + } + + if (json_variant_is_string(variant)) { + const char *n; + + n = json_variant_string(variant); + if (!pkcs11_uri_valid(n)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid RFC7512 PKCS#11 URI.", strna(name)); + + z = strv_new(n); + if (!z) + return log_oom(); + + } else { + + if (!json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string or array of strings.", strna(name)); + + JSON_VARIANT_ARRAY_FOREACH(e, variant) { + const char *n; + + if (!json_variant_is_string(e)) + return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not a string."); + + n = json_variant_string(e); + if (!pkcs11_uri_valid(n)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element in '%s' is not a valid RFC7512 PKCS#11 URI: %s", strna(name), n); + + r = strv_extend(&z, n); + if (r < 0) + return log_oom(); + } + } + + strv_free_and_replace(*l, z); + return 0; +} + +static int dispatch_pkcs11_key_data(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + Pkcs11EncryptedKey *k = userdata; + size_t l; + void *b; + int r; + + if (json_variant_is_null(variant)) { + k->data = mfree(k->data); + k->size = 0; + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + r = unbase64mem(json_variant_string(variant), (size_t) -1, &b, &l); + if (r < 0) + return json_log(variant, flags, r, "Failed to decode encrypted PKCS#11 key: %m"); + + erase_and_free(k->data); + k->data = b; + k->size = l; + + return 0; +} + +static int dispatch_pkcs11_key(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + UserRecord *h = userdata; + JsonVariant *e; + int r; + + if (!json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of objects.", strna(name)); + + JSON_VARIANT_ARRAY_FOREACH(e, variant) { + Pkcs11EncryptedKey *array, *k; + + static const JsonDispatch pkcs11_key_dispatch_table[] = { + { "uri", JSON_VARIANT_STRING, dispatch_pkcs11_uri, offsetof(Pkcs11EncryptedKey, uri), JSON_MANDATORY }, + { "data", JSON_VARIANT_STRING, dispatch_pkcs11_key_data, 0, JSON_MANDATORY }, + { "hashedPassword", JSON_VARIANT_STRING, json_dispatch_string, offsetof(Pkcs11EncryptedKey, hashed_password), JSON_MANDATORY }, + {}, + }; + + if (!json_variant_is_object(e)) + return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not an object."); + + array = reallocarray(h->pkcs11_encrypted_key, h->n_pkcs11_encrypted_key + 1, sizeof(Pkcs11EncryptedKey)); + if (!array) + return log_oom(); + + h->pkcs11_encrypted_key = array; + k = h->pkcs11_encrypted_key + h->n_pkcs11_encrypted_key; + *k = (Pkcs11EncryptedKey) {}; + + r = json_dispatch(e, pkcs11_key_dispatch_table, NULL, flags, k); + if (r < 0) { + pkcs11_encrypted_key_done(k); + return r; + } + + h->n_pkcs11_encrypted_key++; + } + + return 0; +} + +static int dispatch_privileged(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + + static const JsonDispatch privileged_dispatch_table[] = { + { "passwordHint", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, password_hint), 0 }, + { "hashedPassword", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, hashed_password), JSON_SAFE }, + { "sshAuthorizedKeys", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(UserRecord, ssh_authorized_keys), 0 }, + { "pkcs11EncryptedKey", JSON_VARIANT_ARRAY, dispatch_pkcs11_key, 0, 0 }, + {}, + }; + + return json_dispatch(variant, privileged_dispatch_table, NULL, flags, userdata); +} + +static int dispatch_binding(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + + static const JsonDispatch binding_dispatch_table[] = { + { "imagePath", JSON_VARIANT_STRING, json_dispatch_image_path, offsetof(UserRecord, image_path), 0 }, + { "homeDirectory", JSON_VARIANT_STRING, json_dispatch_home_directory, offsetof(UserRecord, home_directory), 0 }, + { "partitionUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 }, + { "luksUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 }, + { "fileSystemUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, file_system_uuid), 0 }, + { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 }, + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 }, + { "storage", JSON_VARIANT_STRING, json_dispatch_storage, offsetof(UserRecord, storage), 0 }, + { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE }, + { "luksCipher", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher), JSON_SAFE }, + { "luksCipherMode", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher_mode), JSON_SAFE }, + { "luksVolumeKeySize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_volume_key_size), 0 }, + {}, + }; + + char smid[SD_ID128_STRING_MAX]; + JsonVariant *m; + sd_id128_t mid; + int r; + + if (!variant) + return 0; + + if (!json_variant_is_object(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name)); + + r = sd_id128_get_machine(&mid); + if (r < 0) + return json_log(variant, flags, r, "Failed to determine machine ID: %m"); + + m = json_variant_by_key(variant, sd_id128_to_string(mid, smid)); + if (!m) + return 0; + + return json_dispatch(m, binding_dispatch_table, NULL, flags, userdata); +} + +int per_machine_id_match(JsonVariant *ids, JsonDispatchFlags flags) { + sd_id128_t mid; + int r; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return json_log(ids, flags, r, "Failed to acquire machine ID: %m"); + + if (json_variant_is_string(ids)) { + sd_id128_t k; + + r = sd_id128_from_string(json_variant_string(ids), &k); + if (r < 0) { + json_log(ids, flags, r, "%s is not a valid machine ID, ignoring: %m", json_variant_string(ids)); + return 0; + } + + return sd_id128_equal(mid, k); + } + + if (json_variant_is_array(ids)) { + JsonVariant *e; + + JSON_VARIANT_ARRAY_FOREACH(e, ids) { + sd_id128_t k; + + if (!json_variant_is_string(e)) { + json_log(e, flags, 0, "Machine ID is not a string, ignoring: %m"); + continue; + } + + r = sd_id128_from_string(json_variant_string(e), &k); + if (r < 0) { + json_log(e, flags, r, "%s is not a valid machine ID, ignoring: %m", json_variant_string(e)); + continue; + } + + if (sd_id128_equal(mid, k)) + return true; + } + + return false; + } + + json_log(ids, flags, 0, "Machine ID is not a string or array of strings, ignoring: %m"); + return false; +} + +int per_machine_hostname_match(JsonVariant *hns, JsonDispatchFlags flags) { + _cleanup_free_ char *hn = NULL; + int r; + + r = gethostname_strict(&hn); + if (r == -ENXIO) { + json_log(hns, flags, r, "No hostname set, not matching perMachine hostname record: %m"); + return false; + } + if (r < 0) + return json_log(hns, flags, r, "Failed to acquire hostname: %m"); + + if (json_variant_is_string(hns)) + return streq(json_variant_string(hns), hn); + + if (json_variant_is_array(hns)) { + JsonVariant *e; + + JSON_VARIANT_ARRAY_FOREACH(e, hns) { + + if (!json_variant_is_string(e)) { + json_log(e, flags, 0, "Hostname is not a string, ignoring: %m"); + continue; + } + + if (streq(json_variant_string(hns), hn)) + return true; + } + + return false; + } + + json_log(hns, flags, 0, "Hostname is not a string or array of strings, ignoring: %m"); + return false; +} + +static int dispatch_per_machine(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + + static const JsonDispatch per_machine_dispatch_table[] = { + { "matchMachineId", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, + { "matchHostname", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 }, + { "iconName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, icon_name), JSON_SAFE }, + { "location", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, location), 0 }, + { "shell", JSON_VARIANT_STRING, json_dispatch_filename_or_path, offsetof(UserRecord, shell), 0 }, + { "umask", JSON_VARIANT_UNSIGNED, json_dispatch_umask, offsetof(UserRecord, umask), 0 }, + { "environment", JSON_VARIANT_ARRAY, json_dispatch_environment, offsetof(UserRecord, environment), 0 }, + { "timeZone", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, time_zone), JSON_SAFE }, + { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, preferred_language), JSON_SAFE }, + { "niceLevel", _JSON_VARIANT_TYPE_INVALID, json_dispatch_nice, offsetof(UserRecord, nice_level), 0 }, + { "resourceLimits", _JSON_VARIANT_TYPE_INVALID, json_dispatch_rlimits, offsetof(UserRecord, rlimits), 0 }, + { "locked", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, locked), 0 }, + { "notBeforeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_before_usec), 0 }, + { "notAfterUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_after_usec), 0 }, + { "storage", JSON_VARIANT_STRING, json_dispatch_storage, offsetof(UserRecord, storage), 0 }, + { "diskSize", JSON_VARIANT_UNSIGNED, json_dispatch_disk_size, offsetof(UserRecord, disk_size), 0 }, + { "diskSizeRelative", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_size_relative), 0 }, + { "skeletonDirectory", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, skeleton_directory), 0 }, + { "accessMode", JSON_VARIANT_UNSIGNED, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 }, + { "tasksMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, tasks_max), 0 }, + { "memoryHigh", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_high), 0 }, + { "memoryMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_max), 0 }, + { "cpuWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, cpu_weight), 0 }, + { "ioWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, io_weight), 0 }, + { "mountNoDevices", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nodev), 0 }, + { "mountNoSuid", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nosuid), 0 }, + { "mountNoExecute", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, noexec), 0 }, + { "cifsDomain", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_domain), JSON_SAFE }, + { "cifsUserName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_user_name), JSON_SAFE }, + { "cifsService", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_service), JSON_SAFE }, + { "imagePath", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, image_path), 0 }, + { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 }, + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 }, + { "memberOf", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(UserRecord, member_of), 0 }, + { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE }, + { "partitionUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 }, + { "luksUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 }, + { "fileSystemUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, file_system_uuid), 0 }, + { "luksDiscard", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, luks_discard), 0, }, + { "luksCipher", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher), JSON_SAFE }, + { "luksCipherMode", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher_mode), JSON_SAFE }, + { "luksVolumeKeySize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_volume_key_size), 0 }, + { "luksPbkdfHashAlgorithm", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_hash_algorithm), JSON_SAFE }, + { "luksPbkdfType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_type), JSON_SAFE }, + { "luksPbkdfTimeCostUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_time_cost_usec), 0 }, + { "luksPbkdfMemoryCost", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_memory_cost), 0 }, + { "luksPbkdfParallelThreads", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_parallel_threads), 0 }, + { "rateLimitIntervalUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_interval_usec), 0 }, + { "rateLimitBurst", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_burst), 0 }, + { "enforcePasswordPolicy", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, enforce_password_policy), 0 }, + { "autoLogin", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, auto_login), 0 }, + { "stopDelayUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, stop_delay_usec), 0 }, + { "killProcesses", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, kill_processes), 0 }, + { "passwordChangeMinUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_min_usec), 0 }, + { "passwordChangeMaxUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_max_usec), 0 }, + { "passwordChangeWarnUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_warn_usec), 0 }, + { "passwordChangeInactiveUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_inactive_usec), 0 }, + { "passwordChangeNow", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, password_change_now), 0 }, + { "pkcs11TokenUri", JSON_VARIANT_ARRAY, dispatch_pkcs11_uri_array, offsetof(UserRecord, pkcs11_token_uri), 0 }, + {}, + }; + + JsonVariant *e; + int r; + + if (!variant) + return 0; + + if (!json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name)); + + JSON_VARIANT_ARRAY_FOREACH(e, variant) { + bool matching = false; + JsonVariant *m; + + if (!json_variant_is_object(e)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of objects.", strna(name)); + + m = json_variant_by_key(e, "matchMachineId"); + if (m) { + r = per_machine_id_match(m, flags); + if (r < 0) + return r; + + matching = r > 0; + } + + if (!matching) { + m = json_variant_by_key(e, "matchHostname"); + if (m) { + r = per_machine_hostname_match(m, flags); + if (r < 0) + return r; + + matching = r > 0; + } + } + + if (!matching) + continue; + + r = json_dispatch(e, per_machine_dispatch_table, NULL, flags, userdata); + if (r < 0) + return r; + } + + return 0; +} + +static int dispatch_status(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + + static const JsonDispatch status_dispatch_table[] = { + { "diskUsage", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_usage), 0 }, + { "diskFree", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_free), 0 }, + { "diskSize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_size), 0 }, + { "diskCeiling", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_ceiling), 0 }, + { "diskFloor", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_floor), 0 }, + { "state", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, state), JSON_SAFE }, + { "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, service), JSON_SAFE }, + { "signedLocally", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, signed_locally), 0 }, + { "goodAuthenticationCounter", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, good_authentication_counter), 0 }, + { "badAuthenticationCounter", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, bad_authentication_counter), 0 }, + { "lastGoodAuthenticationUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_good_authentication_usec), 0 }, + { "lastBadAuthenticationUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_bad_authentication_usec), 0 }, + { "rateLimitBeginUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_begin_usec), 0 }, + { "rateLimitCount", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_count), 0 }, + { "removable", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, removable), 0 }, + {}, + }; + + char smid[SD_ID128_STRING_MAX]; + JsonVariant *m; + sd_id128_t mid; + int r; + + if (!variant) + return 0; + + if (!json_variant_is_object(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name)); + + r = sd_id128_get_machine(&mid); + if (r < 0) + return json_log(variant, flags, r, "Failed to determine machine ID: %m"); + + m = json_variant_by_key(variant, sd_id128_to_string(mid, smid)); + if (!m) + return 0; + + return json_dispatch(m, status_dispatch_table, NULL, flags, userdata); +} + +static int user_record_augment(UserRecord *h, JsonDispatchFlags json_flags) { + assert(h); + + if (!FLAGS_SET(h->mask, USER_RECORD_REGULAR)) + return 0; + + assert(h->user_name); + + if (!h->user_name_and_realm_auto && h->realm) { + h->user_name_and_realm_auto = strjoin(h->user_name, "@", h->realm); + if (!h->user_name_and_realm_auto) + return json_log_oom(h->json, json_flags); + } + + /* Let's add in the following automatisms only for regular users, they dont make sense for any others */ + if (user_record_disposition(h) != USER_REGULAR) + return 0; + + if (!h->home_directory && !h->home_directory_auto) { + h->home_directory_auto = path_join("/home/", h->user_name); + if (!h->home_directory_auto) + return json_log_oom(h->json, json_flags); + } + + if (!h->image_path && !h->image_path_auto) { + const char *suffix; + UserStorage storage; + + storage = user_record_storage(h); + if (storage == USER_LUKS) + suffix = ".home"; + else if (IN_SET(storage, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT)) + suffix = ".homedir"; + else + suffix = NULL; + + if (suffix) { + h->image_path_auto = strjoin("/home/", user_record_user_name_and_realm(h), suffix); + if (!h->image_path_auto) + return json_log_oom(h->json, json_flags); + } + } + + return 0; +} + +int user_group_record_mangle( + JsonVariant *v, + UserRecordLoadFlags load_flags, + JsonVariant **ret_variant, + UserRecordMask *ret_mask) { + + static const struct { + UserRecordMask mask; + const char *name; + } mask_field[] = { + { USER_RECORD_PRIVILEGED, "privileged" }, + { USER_RECORD_SECRET, "secret" }, + { USER_RECORD_BINDING, "binding" }, + { USER_RECORD_PER_MACHINE, "perMachine" }, + { USER_RECORD_STATUS, "status" }, + { USER_RECORD_SIGNATURE, "signature" }, + }; + + JsonDispatchFlags json_flags = USER_RECORD_LOAD_FLAGS_TO_JSON_DISPATCH_FLAGS(load_flags); + _cleanup_(json_variant_unrefp) JsonVariant *w = NULL; + JsonVariant *array[ELEMENTSOF(mask_field) * 2]; + size_t n_retain = 0, i; + UserRecordMask m = 0; + int r; + + assert((load_flags & _USER_RECORD_MASK_MAX) == 0); /* detect mistakes when accidentally passing + * UserRecordMask bit masks as UserRecordLoadFlags + * value */ + + assert(v); + assert(ret_variant); + assert(ret_mask); + + /* Note that this function is shared with the group record parser, hence we try to be generic in our + * log message wording here, to cover both cases. */ + + if (!json_variant_is_object(v)) + return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record is not a JSON object, refusing."); + + if (USER_RECORD_ALLOW_MASK(load_flags) == 0) /* allow nothing? */ + return json_log(v, json_flags, SYNTHETIC_ERRNO(EINVAL), "Nothing allowed in record, refusing."); + + if (USER_RECORD_STRIP_MASK(load_flags) == _USER_RECORD_MASK_MAX) /* strip everything? */ + return json_log(v, json_flags, SYNTHETIC_ERRNO(EINVAL), "Stripping everything from record, refusing."); + + /* Check if we have the special sections and if they match our flags set */ + for (i = 0; i < ELEMENTSOF(mask_field); i++) { + JsonVariant *e, *k; + + if (FLAGS_SET(USER_RECORD_STRIP_MASK(load_flags), mask_field[i].mask)) { + if (!w) + w = json_variant_ref(v); + + r = json_variant_filter(&w, STRV_MAKE(mask_field[i].name)); + if (r < 0) + return json_log(w, json_flags, r, "Failed to remove field from variant: %m"); + + continue; + } + + e = json_variant_by_key_full(v, mask_field[i].name, &k); + if (e) { + if (!FLAGS_SET(USER_RECORD_ALLOW_MASK(load_flags), mask_field[i].mask)) + return json_log(e, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record contains '%s' field, which is not allowed.", mask_field[i].name); + + if (FLAGS_SET(load_flags, USER_RECORD_STRIP_REGULAR)) { + array[n_retain++] = k; + array[n_retain++] = e; + } + + m |= mask_field[i].mask; + } else { + if (FLAGS_SET(USER_RECORD_REQUIRE_MASK(load_flags), mask_field[i].mask)) + return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record lacks '%s' field, which is required.", mask_field[i].name); + } + } + + if (FLAGS_SET(load_flags, USER_RECORD_STRIP_REGULAR)) { + /* If we are supposed to strip regular items, then let's instead just allocate a new object + * with just the stuff we need. */ + + w = json_variant_unref(w); + r = json_variant_new_object(&w, array, n_retain); + if (r < 0) + return json_log(v, json_flags, r, "Failed to allocate new object: %m"); + } else { + /* And now check if there's anything else in the record */ + for (i = 0; i < json_variant_elements(v); i += 2) { + const char *f; + bool special = false; + size_t j; + + assert_se(f = json_variant_string(json_variant_by_index(v, i))); + + for (j = 0; j < ELEMENTSOF(mask_field); j++) + if (streq(f, mask_field[j].name)) { /* already covered in the loop above */ + special = true; + continue; + } + + if (!special) { + if ((load_flags & (USER_RECORD_ALLOW_REGULAR|USER_RECORD_REQUIRE_REGULAR)) == 0) + return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record contains '%s' field, which is not allowed.", f); + + m |= USER_RECORD_REGULAR; + break; + } + } + } + + if (FLAGS_SET(load_flags, USER_RECORD_REQUIRE_REGULAR) && !FLAGS_SET(m, USER_RECORD_REGULAR)) + return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record lacks basic identity fields, which are required."); + + if (m == 0) + return json_log(v, json_flags, SYNTHETIC_ERRNO(EBADMSG), "Record is empty."); + + if (w) + *ret_variant = TAKE_PTR(w); + else + *ret_variant = json_variant_ref(v); + + *ret_mask = m; + return 0; +} + +int user_record_load(UserRecord *h, JsonVariant *v, UserRecordLoadFlags load_flags) { + + static const JsonDispatch user_dispatch_table[] = { + { "userName", JSON_VARIANT_STRING, json_dispatch_user_group_name, offsetof(UserRecord, user_name), 0 }, + { "realm", JSON_VARIANT_STRING, json_dispatch_realm, offsetof(UserRecord, realm), 0 }, + { "realName", JSON_VARIANT_STRING, json_dispatch_gecos, offsetof(UserRecord, real_name), 0 }, + { "emailAddress", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, email_address), JSON_SAFE }, + { "iconName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, icon_name), JSON_SAFE }, + { "location", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, location), 0 }, + { "disposition", JSON_VARIANT_STRING, json_dispatch_user_disposition, offsetof(UserRecord, disposition), 0 }, + { "lastChangeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_change_usec), 0 }, + { "lastPasswordChangeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, last_password_change_usec), 0 }, + { "shell", JSON_VARIANT_STRING, json_dispatch_filename_or_path, offsetof(UserRecord, shell), 0 }, + { "umask", JSON_VARIANT_UNSIGNED, json_dispatch_umask, offsetof(UserRecord, umask), 0 }, + { "environment", JSON_VARIANT_ARRAY, json_dispatch_environment, offsetof(UserRecord, environment), 0 }, + { "timeZone", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, time_zone), JSON_SAFE }, + { "preferredLanguage", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, preferred_language), JSON_SAFE }, + { "niceLevel", _JSON_VARIANT_TYPE_INVALID, json_dispatch_nice, offsetof(UserRecord, nice_level), 0 }, + { "resourceLimits", _JSON_VARIANT_TYPE_INVALID, json_dispatch_rlimits, offsetof(UserRecord, rlimits), 0 }, + { "locked", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, locked), 0 }, + { "notBeforeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_before_usec), 0 }, + { "notAfterUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, not_after_usec), 0 }, + { "storage", JSON_VARIANT_STRING, json_dispatch_storage, offsetof(UserRecord, storage), 0 }, + { "diskSize", JSON_VARIANT_UNSIGNED, json_dispatch_disk_size, offsetof(UserRecord, disk_size), 0 }, + { "diskSizeRelative", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, disk_size_relative), 0 }, + { "skeletonDirectory", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, skeleton_directory), 0 }, + { "accessMode", JSON_VARIANT_UNSIGNED, json_dispatch_access_mode, offsetof(UserRecord, access_mode), 0 }, + { "tasksMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, tasks_max), 0 }, + { "memoryHigh", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_high), 0 }, + { "memoryMax", JSON_VARIANT_UNSIGNED, json_dispatch_tasks_or_memory_max, offsetof(UserRecord, memory_max), 0 }, + { "cpuWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, cpu_weight), 0 }, + { "ioWeight", JSON_VARIANT_UNSIGNED, json_dispatch_weight, offsetof(UserRecord, io_weight), 0 }, + { "mountNoDevices", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nodev), 0 }, + { "mountNoSuid", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, nosuid), 0 }, + { "mountNoExecute", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(UserRecord, noexec), 0 }, + { "cifsDomain", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_domain), JSON_SAFE }, + { "cifsUserName", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_user_name), JSON_SAFE }, + { "cifsService", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, cifs_service), JSON_SAFE }, + { "imagePath", JSON_VARIANT_STRING, json_dispatch_path, offsetof(UserRecord, image_path), 0 }, + { "homeDirectory", JSON_VARIANT_STRING, json_dispatch_home_directory, offsetof(UserRecord, home_directory), 0 }, + { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, uid), 0 }, + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserRecord, gid), 0 }, + { "memberOf", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(UserRecord, member_of), 0 }, + { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, file_system_type), JSON_SAFE }, + { "partitionUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, partition_uuid), 0 }, + { "luksUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, luks_uuid), 0 }, + { "fileSystemUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(UserRecord, file_system_uuid), 0 }, + { "luksDiscard", _JSON_VARIANT_TYPE_INVALID, json_dispatch_tristate, offsetof(UserRecord, luks_discard), 0 }, + { "luksCipher", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher), JSON_SAFE }, + { "luksCipherMode", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_cipher_mode), JSON_SAFE }, + { "luksVolumeKeySize", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_volume_key_size), 0 }, + { "luksPbkdfHashAlgorithm", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_hash_algorithm), JSON_SAFE }, + { "luksPbkdfType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, luks_pbkdf_type), JSON_SAFE }, + { "luksPbkdfTimeCostUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_time_cost_usec), 0 }, + { "luksPbkdfMemoryCost", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_memory_cost), 0 }, + { "luksPbkdfParallelThreads", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, luks_pbkdf_parallel_threads), 0 }, + { "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserRecord, service), JSON_SAFE }, + { "rateLimitIntervalUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_interval_usec), 0 }, + { "rateLimitBurst", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, ratelimit_burst), 0 }, + { "enforcePasswordPolicy", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, enforce_password_policy), 0 }, + { "autoLogin", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, auto_login), 0 }, + { "stopDelayUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, stop_delay_usec), 0 }, + { "killProcesses", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, kill_processes), 0 }, + { "passwordChangeMinUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_min_usec), 0 }, + { "passwordChangeMaxUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_max_usec), 0 }, + { "passwordChangeWarnUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_warn_usec), 0 }, + { "passwordChangeInactiveUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserRecord, password_change_inactive_usec), 0 }, + { "passwordChangeNow", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(UserRecord, password_change_now), 0 }, + { "pkcs11TokenUri", JSON_VARIANT_ARRAY, dispatch_pkcs11_uri_array, offsetof(UserRecord, pkcs11_token_uri), 0 }, + + { "secret", JSON_VARIANT_OBJECT, dispatch_secret, 0, 0 }, + { "privileged", JSON_VARIANT_OBJECT, dispatch_privileged, 0, 0 }, + + /* Ignore the perMachine, binding, status stuff here, and process it later, so that it overrides whatever is set above */ + { "perMachine", JSON_VARIANT_ARRAY, NULL, 0, 0 }, + { "binding", JSON_VARIANT_OBJECT, NULL, 0, 0 }, + { "status", JSON_VARIANT_OBJECT, NULL, 0, 0 }, + + /* Ignore 'signature', we check it with explicit accessors instead */ + { "signature", JSON_VARIANT_ARRAY, NULL, 0, 0 }, + {}, + }; + + JsonDispatchFlags json_flags = USER_RECORD_LOAD_FLAGS_TO_JSON_DISPATCH_FLAGS(load_flags); + int r; + + assert(h); + assert(!h->json); + + /* Note that this call will leave a half-initialized record around on failure! */ + + r = user_group_record_mangle(v, load_flags, &h->json, &h->mask); + if (r < 0) + return r; + + r = json_dispatch(h->json, user_dispatch_table, NULL, json_flags, h); + if (r < 0) + return r; + + /* During the parsing operation above we ignored the 'perMachine', 'binding' and 'status' fields, + * since we want them to override the global options. Let's process them now. */ + + r = dispatch_per_machine("perMachine", json_variant_by_key(h->json, "perMachine"), json_flags, h); + if (r < 0) + return r; + + r = dispatch_binding("binding", json_variant_by_key(h->json, "binding"), json_flags, h); + if (r < 0) + return r; + + r = dispatch_status("status", json_variant_by_key(h->json, "status"), json_flags, h); + if (r < 0) + return r; + + if (FLAGS_SET(h->mask, USER_RECORD_REGULAR) && !h->user_name) + return json_log(h->json, json_flags, SYNTHETIC_ERRNO(EINVAL), "User name field missing, refusing."); + + r = user_record_augment(h, json_flags); + if (r < 0) + return r; + + return 0; +} + +int user_record_build(UserRecord **ret, ...) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *u = NULL; + va_list ap; + int r; + + assert(ret); + + va_start(ap, ret); + r = json_buildv(&v, ap); + va_end(ap); + + if (r < 0) + return r; + + u = user_record_new(); + if (!u) + return -ENOMEM; + + r = user_record_load(u, v, USER_RECORD_LOAD_FULL); + if (r < 0) + return r; + + *ret = TAKE_PTR(u); + return 0; +} + +const char *user_record_user_name_and_realm(UserRecord *h) { + assert(h); + + /* Return the pre-initialized joined string if it is defined */ + if (h->user_name_and_realm_auto) + return h->user_name_and_realm_auto; + + /* If it's not defined then we cannot have a realm */ + assert(!h->realm); + return h->user_name; +} + +UserStorage user_record_storage(UserRecord *h) { + assert(h); + + if (h->storage >= 0) + return h->storage; + + return USER_CLASSIC; +} + +const char *user_record_file_system_type(UserRecord *h) { + assert(h); + + return h->file_system_type ?: "ext4"; +} + +const char *user_record_skeleton_directory(UserRecord *h) { + assert(h); + + return h->skeleton_directory ?: "/etc/skel"; +} + +mode_t user_record_access_mode(UserRecord *h) { + assert(h); + + return h->access_mode != (mode_t) -1 ? h->access_mode : 0700; +} + +const char* user_record_home_directory(UserRecord *h) { + assert(h); + + if (h->home_directory) + return h->home_directory; + if (h->home_directory_auto) + return h->home_directory_auto; + + /* The root user is special, hence be special about it */ + if (streq_ptr(h->user_name, "root")) + return "/root"; + + return "/"; +} + +const char *user_record_image_path(UserRecord *h) { + assert(h); + + if (h->image_path) + return h->image_path; + if (h->image_path_auto) + return h->image_path_auto; + + return IN_SET(user_record_storage(h), USER_CLASSIC, USER_DIRECTORY, USER_SUBVOLUME, USER_FSCRYPT) ? user_record_home_directory(h) : NULL; +} + +const char *user_record_cifs_user_name(UserRecord *h) { + assert(h); + + return h->cifs_user_name ?: h->user_name; +} + +unsigned long user_record_mount_flags(UserRecord *h) { + assert(h); + + return (h->nosuid ? MS_NOSUID : 0) | + (h->noexec ? MS_NOEXEC : 0) | + (h->nodev ? MS_NODEV : 0); +} + +const char *user_record_shell(UserRecord *h) { + assert(h); + + if (h->shell) + return h->shell; + + if (streq_ptr(h->user_name, "root")) + return "/bin/sh"; + + if (user_record_disposition(h) == USER_REGULAR) + return "/bin/bash"; + + return NOLOGIN; +} + +const char *user_record_real_name(UserRecord *h) { + assert(h); + + return h->real_name ?: h->user_name; +} + +bool user_record_luks_discard(UserRecord *h) { + const char *ip; + + assert(h); + + if (h->luks_discard >= 0) + return h->luks_discard; + + ip = user_record_image_path(h); + if (!ip) + return false; + + /* Use discard by default if we are referring to a real block device, but not when operating on a + * loopback device. We want to optimize for SSD and flash storage after all, but we should be careful + * when storing stuff on top of regular file systems in loopback files as doing discard then would + * mean thin provisioning and we should not do that willy-nilly since it means we'll risk EIO later + * on should the disk space to back our file systems not be available. */ + + return path_startswith(ip, "/dev/"); +} + +const char *user_record_luks_cipher(UserRecord *h) { + assert(h); + + return h->luks_cipher ?: "aes"; +} + +const char *user_record_luks_cipher_mode(UserRecord *h) { + assert(h); + + return h->luks_cipher_mode ?: "xts-plain64"; +} + +uint64_t user_record_luks_volume_key_size(UserRecord *h) { + assert(h); + + /* We return a value here that can be cast without loss into size_t which is what libcrypsetup expects */ + + if (h->luks_volume_key_size == UINT64_MAX) + return 256 / 8; + + return MIN(h->luks_volume_key_size, SIZE_MAX); +} + +const char* user_record_luks_pbkdf_type(UserRecord *h) { + assert(h); + + return h->luks_pbkdf_type ?: "argon2i"; +} + +uint64_t user_record_luks_pbkdf_time_cost_usec(UserRecord *h) { + assert(h); + + /* Returns a value with ms granularity, since that's what libcryptsetup expects */ + + if (h->luks_pbkdf_time_cost_usec == UINT64_MAX) + return 500 * USEC_PER_MSEC; /* We default to 500ms, in contrast to libcryptsetup's 2s, which is just awfully slow on every login */ + + return MIN(DIV_ROUND_UP(h->luks_pbkdf_time_cost_usec, USEC_PER_MSEC), UINT32_MAX) * USEC_PER_MSEC; +} + +uint64_t user_record_luks_pbkdf_memory_cost(UserRecord *h) { + assert(h); + + /* Returns a value with kb granularity, since that's what libcryptsetup expects */ + + if (h->luks_pbkdf_memory_cost == UINT64_MAX) + return 64*1024*1024; /* We default to 64M, since this should work on smaller systems too */ + + return MIN(DIV_ROUND_UP(h->luks_pbkdf_memory_cost, 1024), UINT32_MAX) * 1024; +} + +uint64_t user_record_luks_pbkdf_parallel_threads(UserRecord *h) { + assert(h); + + if (h->luks_pbkdf_memory_cost == UINT64_MAX) + return 1; /* We default to 1, since this should work on smaller systems too */ + + return MIN(h->luks_pbkdf_parallel_threads, UINT32_MAX); +} + +const char *user_record_luks_pbkdf_hash_algorithm(UserRecord *h) { + assert(h); + + return h->luks_pbkdf_hash_algorithm ?: "sha512"; +} + +gid_t user_record_gid(UserRecord *h) { + assert(h); + + if (gid_is_valid(h->gid)) + return h->gid; + + return (gid_t) h->uid; +} + +UserDisposition user_record_disposition(UserRecord *h) { + assert(h); + + if (h->disposition >= 0) + return h->disposition; + + /* If not declared, derive from UID */ + + if (!uid_is_valid(h->uid)) + return _USER_DISPOSITION_INVALID; + + if (h->uid == 0 || h->uid == UID_NOBODY) + return USER_INTRINSIC; + + if (uid_is_system(h->uid)) + return USER_SYSTEM; + + if (uid_is_dynamic(h->uid)) + return USER_DYNAMIC; + + if (uid_is_container(h->uid)) + return USER_CONTAINER; + + if (h->uid > INT32_MAX) + return USER_RESERVED; + + return USER_REGULAR; +} + +int user_record_removable(UserRecord *h) { + UserStorage storage; + assert(h); + + if (h->removable >= 0) + return h->removable; + + /* Refuse to decide for classic records */ + storage = user_record_storage(h); + if (h->storage < 0 || h->storage == USER_CLASSIC) + return -1; + + /* For now consider only LUKS home directories with a reference by path as removable */ + return storage == USER_LUKS && path_startswith(user_record_image_path(h), "/dev/"); +} + +uint64_t user_record_ratelimit_interval_usec(UserRecord *h) { + assert(h); + + if (h->ratelimit_interval_usec == UINT64_MAX) + return DEFAULT_RATELIMIT_INTERVAL_USEC; + + return h->ratelimit_interval_usec; +} + +uint64_t user_record_ratelimit_burst(UserRecord *h) { + assert(h); + + if (h->ratelimit_burst == UINT64_MAX) + return DEFAULT_RATELIMIT_BURST; + + return h->ratelimit_burst; +} + +bool user_record_can_authenticate(UserRecord *h) { + assert(h); + + /* Returns true if there's some form of property configured that the user can authenticate against */ + + if (h->n_pkcs11_encrypted_key > 0) + return true; + + return !strv_isempty(h->hashed_password); +} + +uint64_t user_record_ratelimit_next_try(UserRecord *h) { + assert(h); + + /* Calculates when the it's possible to login next. Returns: + * + * UINT64_MAX → Nothing known + * 0 → Right away + * Any other → Next time in CLOCK_REALTIME in usec (which could be in the past) + */ + + if (h->ratelimit_begin_usec == UINT64_MAX || + h->ratelimit_count == UINT64_MAX) + return UINT64_MAX; + + if (h->ratelimit_count < user_record_ratelimit_burst(h)) + return 0; + + return usec_add(h->ratelimit_begin_usec, user_record_ratelimit_interval_usec(h)); +} + +bool user_record_equal(UserRecord *a, UserRecord *b) { + assert(a); + assert(b); + + /* We assume that when a record is modified its JSON data is updated at the same time, hence it's + * sufficient to compare the JSON data. */ + + return json_variant_equal(a->json, b->json); +} + +bool user_record_compatible(UserRecord *a, UserRecord *b) { + assert(a); + assert(b); + + /* If either lacks a the regular section, we can't really decide, let's hence say they are + * incompatible. */ + if (!(a->mask & b->mask & USER_RECORD_REGULAR)) + return false; + + return streq_ptr(a->user_name, b->user_name) && + streq_ptr(a->realm, b->realm); +} + +int user_record_compare_last_change(UserRecord *a, UserRecord *b) { + assert(a); + assert(b); + + if (a->last_change_usec == b->last_change_usec) + return 0; + + /* Always consider a record with a timestamp newer than one without */ + if (a->last_change_usec == UINT64_MAX) + return -1; + if (b->last_change_usec == UINT64_MAX) + return 1; + + return CMP(a->last_change_usec, b->last_change_usec); +} + +int user_record_clone(UserRecord *h, UserRecordLoadFlags flags, UserRecord **ret) { + _cleanup_(user_record_unrefp) UserRecord *c = NULL; + int r; + + assert(h); + assert(ret); + + c = user_record_new(); + if (!c) + return -ENOMEM; + + r = user_record_load(c, h->json, flags); + if (r < 0) + return r; + + *ret = TAKE_PTR(c); + return 0; +} + +int user_record_masked_equal(UserRecord *a, UserRecord *b, UserRecordMask mask) { + _cleanup_(user_record_unrefp) UserRecord *x = NULL, *y = NULL; + int r; + + assert(a); + assert(b); + + /* Compares the two records, but ignores anything not listed in the specified mask */ + + if ((a->mask & ~mask) != 0) { + r = user_record_clone(a, USER_RECORD_ALLOW(mask) | USER_RECORD_STRIP(~mask & _USER_RECORD_MASK_MAX), &x); + if (r < 0) + return r; + + a = x; + } + + if ((b->mask & ~mask) != 0) { + r = user_record_clone(b, USER_RECORD_ALLOW(mask) | USER_RECORD_STRIP(~mask & _USER_RECORD_MASK_MAX), &y); + if (r < 0) + return r; + + b = y; + } + + return user_record_equal(a, b); +} + +int user_record_test_blocked(UserRecord *h) { + usec_t n; + + /* Checks whether access to the specified user shall be allowed at the moment. Returns: + * + * -ESTALE: Record is from the future + * -ENOLCK: Record is blocked + * -EL2HLT: Record is not valid yet + * -EL3HLT: Record is not valid anymore + * + */ + + assert(h); + + n = now(CLOCK_REALTIME); + if (h->last_change_usec != UINT64_MAX && + h->last_change_usec > n) /* Don't allow log ins when the record is from the future */ + return -ESTALE; + + if (h->locked > 0) + return -ENOLCK; + + if (h->not_before_usec != UINT64_MAX && n < h->not_before_usec) + return -EL2HLT; + if (h->not_after_usec != UINT64_MAX && n > h->not_after_usec) + return -EL3HLT; + + return 0; +} + +int user_record_test_password_change_required(UserRecord *h) { + bool change_permitted; + usec_t n; + + assert(h); + + /* Checks whether the user must change the password when logging in + + -EKEYREVOKED: Change password now because admin said so + -EOWNERDEAD: Change password now because it expired + -EKEYREJECTED: Password is expired, no changing is allowed + -EKEYEXPIRED: Password is about to expire, warn user + -ENETDOWN: Record has expiration info but no password change timestamp + -EROFS: No password change required nor permitted + 0: No password change required, but permitted + */ + + /* If a pasword change request has been set explicitly, it overrides everything */ + if (h->password_change_now > 0) + return -EKEYREVOKED; + + n = now(CLOCK_REALTIME); + + /* Then, let's check if password changing is currently allowed at all */ + if (h->password_change_min_usec != UINT64_MAX) { + + /* Expiry configured but no password change timestamp known? */ + if (h->last_password_change_usec == UINT64_MAX) + return -ENETDOWN; + + if (h->password_change_min_usec >= UINT64_MAX - h->last_password_change_usec) + change_permitted = false; + else + change_permitted = n >= h->last_password_change_usec + h->password_change_min_usec; + + } else + change_permitted = true; + + /* Let's check whether the password has expired. */ + if (!(h->password_change_max_usec == UINT64_MAX || + h->password_change_max_usec >= UINT64_MAX - h->last_password_change_usec)) { + + uint64_t change_before; + + /* Expiry configured but no password change timestamp known? */ + if (h->last_password_change_usec == UINT64_MAX) + return -ENETDOWN; + + /* Password is in inactive phase? */ + if (h->password_change_inactive_usec != UINT64_MAX && + h->password_change_inactive_usec < UINT64_MAX - h->password_change_max_usec) { + usec_t added; + + added = h->password_change_inactive_usec + h->password_change_max_usec; + if (added < UINT64_MAX - h->last_password_change_usec && + n >= h->last_password_change_usec + added) + return -EKEYREJECTED; + } + + /* Password needs to be changed now? */ + change_before = h->last_password_change_usec + h->password_change_max_usec; + if (n >= change_before) + return change_permitted ? -EOWNERDEAD : -EKEYREJECTED; + + /* Warn user? */ + if (h->password_change_warn_usec != UINT64_MAX && + (change_before < h->password_change_warn_usec || + n >= change_before - h->password_change_warn_usec)) + return change_permitted ? -EKEYEXPIRED : -EROFS; + } + + /* No password changing necessary */ + return change_permitted ? 0 : -EROFS; +} + +static const char* const user_storage_table[_USER_STORAGE_MAX] = { + [USER_CLASSIC] = "classic", + [USER_LUKS] = "luks", + [USER_DIRECTORY] = "directory", + [USER_SUBVOLUME] = "subvolume", + [USER_FSCRYPT] = "fscrypt", + [USER_CIFS] = "cifs", +}; + +DEFINE_STRING_TABLE_LOOKUP(user_storage, UserStorage); + +static const char* const user_disposition_table[_USER_DISPOSITION_MAX] = { + [USER_INTRINSIC] = "intrinsic", + [USER_SYSTEM] = "system", + [USER_DYNAMIC] = "dynamic", + [USER_REGULAR] = "regular", + [USER_CONTAINER] = "container", + [USER_RESERVED] = "reserved", +}; + +DEFINE_STRING_TABLE_LOOKUP(user_disposition, UserDisposition); diff --git a/src/shared/user-record.h b/src/shared/user-record.h new file mode 100644 index 0000000000..5bac304767 --- /dev/null +++ b/src/shared/user-record.h @@ -0,0 +1,375 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include +#include + +#include "sd-id128.h" + +#include "json.h" +#include "missing_resource.h" +#include "time-util.h" + +/* But some limits on disk sizes: not less than 5M, not more than 5T */ +#define USER_DISK_SIZE_MIN (UINT64_C(5)*1024*1024) +#define USER_DISK_SIZE_MAX (UINT64_C(5)*1024*1024*1024*1024) + +/* The default disk size to use when nothing else is specified, relative to free disk space */ +#define USER_DISK_SIZE_DEFAULT_PERCENT 85 + +typedef enum UserDisposition { + USER_INTRINSIC, /* root and nobody */ + USER_SYSTEM, /* statically allocated users for system services */ + USER_DYNAMIC, /* dynamically allocated users for system services */ + USER_REGULAR, /* regular (typically human users) */ + USER_CONTAINER, /* UID ranges allocated for container uses */ + USER_RESERVED, /* Range above 2^31 */ + _USER_DISPOSITION_MAX, + _USER_DISPOSITION_INVALID = -1, +} UserDisposition; + +typedef enum UserHomeStorage { + USER_CLASSIC, + USER_LUKS, + USER_DIRECTORY, /* A directory, and a .identity file in it, which USER_CLASSIC lacks */ + USER_SUBVOLUME, + USER_FSCRYPT, + USER_CIFS, + _USER_STORAGE_MAX, + _USER_STORAGE_INVALID = -1 +} UserStorage; + +typedef enum UserRecordMask { + /* The various sections an identity record may have, as bit mask */ + USER_RECORD_REGULAR = 1U << 0, + USER_RECORD_SECRET = 1U << 1, + USER_RECORD_PRIVILEGED = 1U << 2, + USER_RECORD_PER_MACHINE = 1U << 3, + USER_RECORD_BINDING = 1U << 4, + USER_RECORD_STATUS = 1U << 5, + USER_RECORD_SIGNATURE = 1U << 6, + _USER_RECORD_MASK_MAX = (1U << 7)-1 +} UserRecordMask; + +typedef enum UserRecordLoadFlags { + /* A set of flags used while loading a user record from JSON data. We leave the lower 6 bits free, + * just as a safety precaution so that we can detect borked conversions between UserRecordMask and + * UserRecordLoadFlags. */ + + /* What to require */ + USER_RECORD_REQUIRE_REGULAR = USER_RECORD_REGULAR << 7, + USER_RECORD_REQUIRE_SECRET = USER_RECORD_SECRET << 7, + USER_RECORD_REQUIRE_PRIVILEGED = USER_RECORD_PRIVILEGED << 7, + USER_RECORD_REQUIRE_PER_MACHINE = USER_RECORD_PER_MACHINE << 7, + USER_RECORD_REQUIRE_BINDING = USER_RECORD_BINDING << 7, + USER_RECORD_REQUIRE_STATUS = USER_RECORD_STATUS << 7, + USER_RECORD_REQUIRE_SIGNATURE = USER_RECORD_SIGNATURE << 7, + + /* What to allow */ + USER_RECORD_ALLOW_REGULAR = USER_RECORD_REGULAR << 14, + USER_RECORD_ALLOW_SECRET = USER_RECORD_SECRET << 14, + USER_RECORD_ALLOW_PRIVILEGED = USER_RECORD_PRIVILEGED << 14, + USER_RECORD_ALLOW_PER_MACHINE = USER_RECORD_PER_MACHINE << 14, + USER_RECORD_ALLOW_BINDING = USER_RECORD_BINDING << 14, + USER_RECORD_ALLOW_STATUS = USER_RECORD_STATUS << 14, + USER_RECORD_ALLOW_SIGNATURE = USER_RECORD_SIGNATURE << 14, + + /* What to strip */ + USER_RECORD_STRIP_REGULAR = USER_RECORD_REGULAR << 21, + USER_RECORD_STRIP_SECRET = USER_RECORD_SECRET << 21, + USER_RECORD_STRIP_PRIVILEGED = USER_RECORD_PRIVILEGED << 21, + USER_RECORD_STRIP_PER_MACHINE = USER_RECORD_PER_MACHINE << 21, + USER_RECORD_STRIP_BINDING = USER_RECORD_BINDING << 21, + USER_RECORD_STRIP_STATUS = USER_RECORD_STATUS << 21, + USER_RECORD_STRIP_SIGNATURE = USER_RECORD_SIGNATURE << 21, + + /* Some special combinations that deserve explicit names */ + USER_RECORD_LOAD_FULL = USER_RECORD_REQUIRE_REGULAR | + USER_RECORD_ALLOW_SECRET | + USER_RECORD_ALLOW_PRIVILEGED | + USER_RECORD_ALLOW_PER_MACHINE | + USER_RECORD_ALLOW_BINDING | + USER_RECORD_ALLOW_STATUS | + USER_RECORD_ALLOW_SIGNATURE, + + USER_RECORD_LOAD_REFUSE_SECRET = USER_RECORD_REQUIRE_REGULAR | + USER_RECORD_ALLOW_PRIVILEGED | + USER_RECORD_ALLOW_PER_MACHINE | + USER_RECORD_ALLOW_BINDING | + USER_RECORD_ALLOW_STATUS | + USER_RECORD_ALLOW_SIGNATURE, + + USER_RECORD_LOAD_MASK_SECRET = USER_RECORD_REQUIRE_REGULAR | + USER_RECORD_ALLOW_PRIVILEGED | + USER_RECORD_ALLOW_PER_MACHINE | + USER_RECORD_ALLOW_BINDING | + USER_RECORD_ALLOW_STATUS | + USER_RECORD_ALLOW_SIGNATURE | + USER_RECORD_STRIP_SECRET, + + USER_RECORD_EXTRACT_SECRET = USER_RECORD_REQUIRE_SECRET | + USER_RECORD_STRIP_REGULAR | + USER_RECORD_STRIP_PRIVILEGED | + USER_RECORD_STRIP_PER_MACHINE | + USER_RECORD_STRIP_BINDING | + USER_RECORD_STRIP_STATUS | + USER_RECORD_STRIP_SIGNATURE, + + USER_RECORD_LOAD_SIGNABLE = USER_RECORD_REQUIRE_REGULAR | + USER_RECORD_ALLOW_PRIVILEGED | + USER_RECORD_ALLOW_PER_MACHINE, + + USER_RECORD_EXTRACT_SIGNABLE = USER_RECORD_LOAD_SIGNABLE | + USER_RECORD_STRIP_SECRET | + USER_RECORD_STRIP_BINDING | + USER_RECORD_STRIP_STATUS | + USER_RECORD_STRIP_SIGNATURE, + + USER_RECORD_LOAD_EMBEDDED = USER_RECORD_REQUIRE_REGULAR | + USER_RECORD_ALLOW_PRIVILEGED | + USER_RECORD_ALLOW_PER_MACHINE | + USER_RECORD_ALLOW_SIGNATURE, + + USER_RECORD_EXTRACT_EMBEDDED = USER_RECORD_LOAD_EMBEDDED | + USER_RECORD_STRIP_SECRET | + USER_RECORD_STRIP_BINDING | + USER_RECORD_STRIP_STATUS, + + /* Whether to log about loader errors beyond LOG_DEBUG */ + USER_RECORD_LOG = 1U << 28, + + /* Whether to ignore errors and load what we can */ + USER_RECORD_PERMISSIVE = 1U << 29, +} UserRecordLoadFlags; + +static inline UserRecordLoadFlags USER_RECORD_REQUIRE(UserRecordMask m) { + assert((m & ~_USER_RECORD_MASK_MAX) == 0); + return m << 7; +} + +static inline UserRecordLoadFlags USER_RECORD_ALLOW(UserRecordMask m) { + assert((m & ~_USER_RECORD_MASK_MAX) == 0); + return m << 14; +} + +static inline UserRecordLoadFlags USER_RECORD_STRIP(UserRecordMask m) { + assert((m & ~_USER_RECORD_MASK_MAX) == 0); + return m << 21; +} + +static inline UserRecordMask USER_RECORD_REQUIRE_MASK(UserRecordLoadFlags f) { + return (f >> 7) & _USER_RECORD_MASK_MAX; +} + +static inline UserRecordMask USER_RECORD_ALLOW_MASK(UserRecordLoadFlags f) { + return ((f >> 14) & _USER_RECORD_MASK_MAX) | USER_RECORD_REQUIRE_MASK(f); +} + +static inline UserRecordMask USER_RECORD_STRIP_MASK(UserRecordLoadFlags f) { + return (f >> 21) & _USER_RECORD_MASK_MAX; +} + +static inline JsonDispatchFlags USER_RECORD_LOAD_FLAGS_TO_JSON_DISPATCH_FLAGS(UserRecordLoadFlags flags) { + return (FLAGS_SET(flags, USER_RECORD_LOG) ? JSON_LOG : 0) | + (FLAGS_SET(flags, USER_RECORD_PERMISSIVE) ? JSON_PERMISSIVE : 0); +} + +typedef struct Pkcs11EncryptedKey { + /* The encrypted passphrase, which can be decrypted with the private key indicated below */ + void *data; + size_t size; + + /* Where to find the private key to decrypt the encrypted passphrase above */ + char *uri; + + /* What to test the decrypted passphrase against to allow access (classic UNIX password hash). Note + * that the decrypted passphrase is also used for unlocking LUKS and fscrypt, and if the account is + * backed by LUKS or fscrypt the hashed password is only an additional layer of authentication, not + * the only. */ + char *hashed_password; +} Pkcs11EncryptedKey; + +typedef struct UserRecord { + /* The following three fields are not part of the JSON record */ + unsigned n_ref; + UserRecordMask mask; + bool incomplete; /* incomplete due to security restrictions. */ + + char *user_name; + char *realm; + char *user_name_and_realm_auto; /* the user_name field concatenated with '@' and the realm, if the latter is defined */ + char *real_name; + char *email_address; + char *password_hint; + char *icon_name; + char *location; + + UserDisposition disposition; + uint64_t last_change_usec; + uint64_t last_password_change_usec; + + char *shell; + mode_t umask; + char **environment; + char *time_zone; + char *preferred_language; + int nice_level; + struct rlimit *rlimits[_RLIMIT_MAX]; + + int locked; /* prohibit activation in general */ + uint64_t not_before_usec; /* prohibit activation before this unix time */ + uint64_t not_after_usec; /* prohibit activation after this unix time */ + + UserStorage storage; + uint64_t disk_size; + uint64_t disk_size_relative; /* Disk size, relative to the free bytes of the medium, normalized to UINT32_MAX = 100% */ + char *skeleton_directory; + mode_t access_mode; + + uint64_t tasks_max; + uint64_t memory_high; + uint64_t memory_max; + uint64_t cpu_weight; + uint64_t io_weight; + + bool nosuid; + bool nodev; + bool noexec; + + char **hashed_password; + char **ssh_authorized_keys; + char **password; + char **pkcs11_pin; + + char *cifs_domain; + char *cifs_user_name; + char *cifs_service; + + char *image_path; + char *image_path_auto; /* when none is configured explicitly, this is where we place the implicit image */ + char *home_directory; + char *home_directory_auto; /* when none is set explicitly, this is where we place the implicit home directory */ + + uid_t uid; + gid_t gid; + + char **member_of; + + char *file_system_type; + sd_id128_t partition_uuid; + sd_id128_t luks_uuid; + sd_id128_t file_system_uuid; + + int luks_discard; + char *luks_cipher; + char *luks_cipher_mode; + uint64_t luks_volume_key_size; + char *luks_pbkdf_hash_algorithm; + char *luks_pbkdf_type; + uint64_t luks_pbkdf_time_cost_usec; + uint64_t luks_pbkdf_memory_cost; + uint64_t luks_pbkdf_parallel_threads; + + uint64_t disk_usage; + uint64_t disk_free; + uint64_t disk_ceiling; + uint64_t disk_floor; + + char *state; + char *service; + int signed_locally; + + uint64_t good_authentication_counter; + uint64_t bad_authentication_counter; + uint64_t last_good_authentication_usec; + uint64_t last_bad_authentication_usec; + + uint64_t ratelimit_begin_usec; + uint64_t ratelimit_count; + uint64_t ratelimit_interval_usec; + uint64_t ratelimit_burst; + + int removable; + int enforce_password_policy; + int auto_login; + + uint64_t stop_delay_usec; /* How long to leave systemd --user around on log-out */ + int kill_processes; /* Whether to kill user processes forcibly on log-out */ + + /* The following exist mostly so that we can cover the full /etc/shadow set of fields */ + uint64_t password_change_min_usec; /* maps to .sp_min */ + uint64_t password_change_max_usec; /* maps to .sp_max */ + uint64_t password_change_warn_usec; /* maps to .sp_warn */ + uint64_t password_change_inactive_usec; /* maps to .sp_inact */ + int password_change_now; /* Require a password change immediately on next login (.sp_lstchg = 0) */ + + char **pkcs11_token_uri; + Pkcs11EncryptedKey *pkcs11_encrypted_key; + size_t n_pkcs11_encrypted_key; + int pkcs11_protected_authentication_path_permitted; + + JsonVariant *json; +} UserRecord; + +UserRecord* user_record_new(void); +UserRecord* user_record_ref(UserRecord *h); +UserRecord* user_record_unref(UserRecord *h); + +DEFINE_TRIVIAL_CLEANUP_FUNC(UserRecord*, user_record_unref); + +int user_record_load(UserRecord *h, JsonVariant *v, UserRecordLoadFlags flags); +int user_record_build(UserRecord **ret, ...); + +const char *user_record_user_name_and_realm(UserRecord *h); +UserStorage user_record_storage(UserRecord *h); +const char *user_record_file_system_type(UserRecord *h); +const char *user_record_skeleton_directory(UserRecord *h); +mode_t user_record_access_mode(UserRecord *h); +const char *user_record_home_directory(UserRecord *h); +const char *user_record_image_path(UserRecord *h); +unsigned long user_record_mount_flags(UserRecord *h); +const char *user_record_cifs_user_name(UserRecord *h); +const char *user_record_shell(UserRecord *h); +const char *user_record_real_name(UserRecord *h); +bool user_record_luks_discard(UserRecord *h); +const char *user_record_luks_cipher(UserRecord *h); +const char *user_record_luks_cipher_mode(UserRecord *h); +uint64_t user_record_luks_volume_key_size(UserRecord *h); +const char* user_record_luks_pbkdf_type(UserRecord *h); +usec_t user_record_luks_pbkdf_time_cost_usec(UserRecord *h); +uint64_t user_record_luks_pbkdf_memory_cost(UserRecord *h); +uint64_t user_record_luks_pbkdf_parallel_threads(UserRecord *h); +const char *user_record_luks_pbkdf_hash_algorithm(UserRecord *h); +gid_t user_record_gid(UserRecord *h); +UserDisposition user_record_disposition(UserRecord *h); +int user_record_removable(UserRecord *h); +usec_t user_record_ratelimit_interval_usec(UserRecord *h); +uint64_t user_record_ratelimit_burst(UserRecord *h); +bool user_record_can_authenticate(UserRecord *h); + +bool user_record_equal(UserRecord *a, UserRecord *b); +bool user_record_compatible(UserRecord *a, UserRecord *b); +int user_record_compare_last_change(UserRecord *a, UserRecord *b); + +usec_t user_record_ratelimit_next_try(UserRecord *h); + +int user_record_clone(UserRecord *h, UserRecordLoadFlags flags, UserRecord **ret); +int user_record_masked_equal(UserRecord *a, UserRecord *b, UserRecordMask mask); + +int user_record_test_blocked(UserRecord *h); +int user_record_test_password_change_required(UserRecord *h); + +/* The following six are user by group-record.c, that's why we export them here */ +int json_dispatch_realm(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); +int json_dispatch_user_group_list(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); +int json_dispatch_user_disposition(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata); + +int per_machine_id_match(JsonVariant *ids, JsonDispatchFlags flags); +int per_machine_hostname_match(JsonVariant *hns, JsonDispatchFlags flags); +int user_group_record_mangle(JsonVariant *v, UserRecordLoadFlags load_flags, JsonVariant **ret_variant, UserRecordMask *ret_mask); + +const char* user_storage_to_string(UserStorage t) _const_; +UserStorage user_storage_from_string(const char *s) _pure_; + +const char* user_disposition_to_string(UserDisposition t) _const_; +UserDisposition user_disposition_from_string(const char *s) _pure_; From 9b2d907877ab10202fdfb7db7a5ce6e8b32c70b8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 5 Aug 2019 18:21:30 +0200 Subject: [PATCH 05/29] shared: add helpers for converting NSS passwd/group structures to new JSON objects These new calls may be used to convert classic UNIX/glibc NSS struct passwd and struct group records into new-style JSON-based user/group objects. --- src/shared/group-record-nss.c | 203 ++++++++++++++++++++++++ src/shared/group-record-nss.h | 15 ++ src/shared/meson.build | 4 + src/shared/user-record-nss.c | 288 ++++++++++++++++++++++++++++++++++ src/shared/user-record-nss.h | 15 ++ 5 files changed, 525 insertions(+) create mode 100644 src/shared/group-record-nss.c create mode 100644 src/shared/group-record-nss.h create mode 100644 src/shared/user-record-nss.c create mode 100644 src/shared/user-record-nss.h diff --git a/src/shared/group-record-nss.c b/src/shared/group-record-nss.c new file mode 100644 index 0000000000..77924f1c40 --- /dev/null +++ b/src/shared/group-record-nss.c @@ -0,0 +1,203 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "errno-util.h" +#include "group-record-nss.h" +#include "libcrypt-util.h" +#include "strv.h" + +int nss_group_to_group_record( + const struct group *grp, + const struct sgrp *sgrp, + GroupRecord **ret) { + + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; + int r; + + assert(grp); + assert(ret); + + if (isempty(grp->gr_name)) + return -EINVAL; + + if (sgrp && !streq_ptr(sgrp->sg_namp, grp->gr_name)) + return -EINVAL; + + g = group_record_new(); + if (!g) + return -ENOMEM; + + g->group_name = strdup(grp->gr_name); + if (!g->group_name) + return -ENOMEM; + + g->members = strv_copy(grp->gr_mem); + if (!g->members) + return -ENOMEM; + + g->gid = grp->gr_gid; + + if (sgrp) { + if (hashed_password_valid(sgrp->sg_passwd)) { + g->hashed_password = strv_new(sgrp->sg_passwd); + if (!g->hashed_password) + return -ENOMEM; + } + + r = strv_extend_strv(&g->members, sgrp->sg_mem, 1); + if (r < 0) + return r; + + g->administrators = strv_copy(sgrp->sg_adm); + if (!g->administrators) + return -ENOMEM; + } + + r = json_build(&g->json, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(g->group_name)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(g->gid)), + JSON_BUILD_PAIR_CONDITION(!strv_isempty(g->members), "members", JSON_BUILD_STRV(g->members)), + JSON_BUILD_PAIR_CONDITION(!strv_isempty(g->hashed_password), "privileged", JSON_BUILD_OBJECT(JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRV(g->hashed_password)))), + JSON_BUILD_PAIR_CONDITION(!strv_isempty(g->administrators), "administrators", JSON_BUILD_STRV(g->administrators)))); + if (r < 0) + return r; + + g->mask = USER_RECORD_REGULAR | + (!strv_isempty(g->hashed_password) ? USER_RECORD_PRIVILEGED : 0); + + *ret = TAKE_PTR(g); + return 0; +} + +int nss_sgrp_for_group(const struct group *grp, struct sgrp *ret_sgrp, char **ret_buffer) { + size_t buflen = 4096; + int r; + + assert(grp); + assert(ret_sgrp); + assert(ret_buffer); + + for (;;) { + _cleanup_free_ char *buf = NULL; + struct sgrp sgrp, *result; + + buf = malloc(buflen); + if (!buf) + return -ENOMEM; + + r = getsgnam_r(grp->gr_name, &sgrp, buf, buflen, &result); + if (r == 0) { + if (!result) + return -ESRCH; + + *ret_sgrp = *result; + *ret_buffer = TAKE_PTR(buf); + return 0; + } + if (r < 0) + return -EIO; /* Weird, this should not return negative! */ + if (r != ERANGE) + return -r; + + if (buflen > SIZE_MAX / 2) + return -ERANGE; + + buflen *= 2; + buf = mfree(buf); + } +} + +int nss_group_record_by_name(const char *name, GroupRecord **ret) { + _cleanup_free_ char *buf = NULL, *sbuf = NULL; + struct group grp, *result; + bool incomplete = false; + size_t buflen = 4096; + struct sgrp sgrp; + int r; + + assert(name); + assert(ret); + + for (;;) { + buf = malloc(buflen); + if (!buf) + return -ENOMEM; + + r = getgrnam_r(name, &grp, buf, buflen, &result); + if (r == 0) { + if (!result) + return -ESRCH; + + break; + } + + if (r < 0) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "getgrnam_r() returned a negative value"); + if (r != ERANGE) + return -r; + if (buflen > SIZE_MAX / 2) + return -ERANGE; + + buflen *= 2; + buf = mfree(buf); + } + + r = nss_sgrp_for_group(result, &sgrp, &sbuf); + if (r < 0) { + log_debug_errno(r, "Failed to do shadow lookup for group %s, ignoring: %m", result->gr_name); + incomplete = ERRNO_IS_PRIVILEGE(r); + } + + r = nss_group_to_group_record(result, r >= 0 ? &sgrp : NULL, ret); + if (r < 0) + return r; + + (*ret)->incomplete = incomplete; + return 0; +} + +int nss_group_record_by_gid(gid_t gid, GroupRecord **ret) { + _cleanup_free_ char *buf = NULL, *sbuf = NULL; + struct group grp, *result; + bool incomplete = false; + size_t buflen = 4096; + struct sgrp sgrp; + int r; + + assert(ret); + + for (;;) { + buf = malloc(buflen); + if (!buf) + return -ENOMEM; + + r = getgrgid_r(gid, &grp, buf, buflen, &result); + if (r == 0) { + if (!result) + return -ESRCH; + break; + } + + if (r < 0) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "getgrgid_r() returned a negative value"); + if (r != ERANGE) + return -r; + if (buflen > SIZE_MAX / 2) + return -ERANGE; + + buflen *= 2; + buf = mfree(buf); + } + + r = nss_sgrp_for_group(result, &sgrp, &sbuf); + if (r < 0) { + log_debug_errno(r, "Failed to do shadow lookup for group %s, ignoring: %m", result->gr_name); + incomplete = ERRNO_IS_PRIVILEGE(r); + } + + r = nss_group_to_group_record(result, r >= 0 ? &sgrp : NULL, ret); + if (r < 0) + return r; + + (*ret)->incomplete = incomplete; + return 0; +} diff --git a/src/shared/group-record-nss.h b/src/shared/group-record-nss.h new file mode 100644 index 0000000000..38b2995178 --- /dev/null +++ b/src/shared/group-record-nss.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include +#include + +#include "group-record.h" + +/* Synthesize GroupRecord objects from NSS data */ + +int nss_group_to_group_record(const struct group *grp, const struct sgrp *sgrp, GroupRecord **ret); +int nss_sgrp_for_group(const struct group *grp, struct sgrp *ret_sgrp, char **ret_buffer); + +int nss_group_record_by_name(const char *name, GroupRecord **ret); +int nss_group_record_by_gid(gid_t gid, GroupRecord **ret); diff --git a/src/shared/meson.build b/src/shared/meson.build index f9799fee1d..d99aaa62c6 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -86,6 +86,8 @@ shared_sources = files(''' generator.c generator.h gpt.h + group-record-nss.c + group-record-nss.h group-record.c group-record.h id128-print.c @@ -191,6 +193,8 @@ shared_sources = files(''' uid-range.h unit-file.c unit-file.h + user-record-nss.c + user-record-nss.h user-record.c user-record.h utmp-wtmp.h diff --git a/src/shared/user-record-nss.c b/src/shared/user-record-nss.c new file mode 100644 index 0000000000..11c085d63d --- /dev/null +++ b/src/shared/user-record-nss.c @@ -0,0 +1,288 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "errno-util.h" +#include "format-util.h" +#include "libcrypt-util.h" +#include "strv.h" +#include "user-record-nss.h" + +int nss_passwd_to_user_record( + const struct passwd *pwd, + const struct spwd *spwd, + UserRecord **ret) { + + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + int r; + + assert(pwd); + assert(ret); + + if (isempty(pwd->pw_name)) + return -EINVAL; + + if (spwd && !streq_ptr(spwd->sp_namp, pwd->pw_name)) + return -EINVAL; + + hr = user_record_new(); + if (!hr) + return -ENOMEM; + + r = free_and_strdup(&hr->user_name, pwd->pw_name); + if (r < 0) + return r; + + if (isempty(pwd->pw_gecos) || streq_ptr(pwd->pw_gecos, hr->user_name)) + hr->real_name = mfree(hr->real_name); + else { + r = free_and_strdup(&hr->real_name, pwd->pw_gecos); + if (r < 0) + return r; + } + + if (isempty(pwd->pw_dir)) + hr->home_directory = mfree(hr->home_directory); + else { + r = free_and_strdup(&hr->home_directory, pwd->pw_dir); + if (r < 0) + return r; + } + + if (isempty(pwd->pw_shell)) + hr->shell = mfree(hr->shell); + else { + r = free_and_strdup(&hr->shell, pwd->pw_shell); + if (r < 0) + return r; + } + + hr->uid = pwd->pw_uid; + hr->gid = pwd->pw_gid; + + if (spwd) { + if (hashed_password_valid(spwd->sp_pwdp)) { + strv_free_erase(hr->hashed_password); + hr->hashed_password = strv_new(spwd->sp_pwdp); + if (!hr->hashed_password) + return -ENOMEM; + } else + hr->hashed_password = strv_free_erase(hr->hashed_password); + + /* shadow-utils suggests using "chage -E 0" (or -E 1, depending on which man page you check) + * for locking a whole account, hence check for that. Note that it also defines a way to lock + * just a password instead of the whole account, but that's mostly pointless in times of + * password-less authorization, hence let's not bother. */ + + if (spwd->sp_expire >= 0) + hr->locked = spwd->sp_expire <= 1; + else + hr->locked = -1; + + if (spwd->sp_expire > 1 && (uint64_t) spwd->sp_expire < (UINT64_MAX-1)/USEC_PER_DAY) + hr->not_after_usec = spwd->sp_expire * USEC_PER_DAY; + else + hr->not_after_usec = UINT64_MAX; + + if (spwd->sp_lstchg >= 0) + hr->password_change_now = spwd->sp_lstchg == 0; + else + hr->password_change_now = -1; + + if (spwd->sp_lstchg > 0 && (uint64_t) spwd->sp_lstchg <= (UINT64_MAX-1)/USEC_PER_DAY) + hr->last_password_change_usec = spwd->sp_lstchg * USEC_PER_DAY; + else + hr->last_password_change_usec = UINT64_MAX; + + if (spwd->sp_min > 0 && (uint64_t) spwd->sp_min <= (UINT64_MAX-1)/USEC_PER_DAY) + hr->password_change_min_usec = spwd->sp_min * USEC_PER_DAY; + else + hr->password_change_min_usec = UINT64_MAX; + + if (spwd->sp_max > 0 && (uint64_t) spwd->sp_max <= (UINT64_MAX-1)/USEC_PER_DAY) + hr->password_change_max_usec = spwd->sp_max * USEC_PER_DAY; + else + hr->password_change_max_usec = UINT64_MAX; + + if (spwd->sp_warn > 0 && (uint64_t) spwd->sp_warn <= (UINT64_MAX-1)/USEC_PER_DAY) + hr->password_change_warn_usec = spwd->sp_warn * USEC_PER_DAY; + else + hr->password_change_warn_usec = UINT64_MAX; + + if (spwd->sp_inact > 0 && (uint64_t) spwd->sp_inact <= (UINT64_MAX-1)/USEC_PER_DAY) + hr->password_change_inactive_usec = spwd->sp_inact * USEC_PER_DAY; + else + hr->password_change_inactive_usec = UINT64_MAX; + } else { + hr->hashed_password = strv_free_erase(hr->hashed_password); + hr->locked = -1; + hr->not_after_usec = UINT64_MAX; + hr->password_change_now = -1, + hr->last_password_change_usec = UINT64_MAX; + hr->password_change_min_usec = UINT64_MAX; + hr->password_change_max_usec = UINT64_MAX; + hr->password_change_warn_usec = UINT64_MAX; + hr->password_change_inactive_usec = UINT64_MAX; + } + + hr->json = json_variant_unref(hr->json); + r = json_build(&hr->json, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(hr->user_name)), + JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(hr->uid)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(hr->gid)), + JSON_BUILD_PAIR_CONDITION(hr->real_name, "realName", JSON_BUILD_STRING(hr->real_name)), + JSON_BUILD_PAIR_CONDITION(hr->home_directory, "homeDirectory", JSON_BUILD_STRING(hr->home_directory)), + JSON_BUILD_PAIR_CONDITION(hr->shell, "shell", JSON_BUILD_STRING(hr->shell)), + JSON_BUILD_PAIR_CONDITION(!strv_isempty(hr->hashed_password), "privileged", JSON_BUILD_OBJECT(JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRV(hr->hashed_password)))), + JSON_BUILD_PAIR_CONDITION(hr->locked >= 0, "locked", JSON_BUILD_BOOLEAN(hr->locked)), + JSON_BUILD_PAIR_CONDITION(hr->not_after_usec != UINT64_MAX, "notAfterUSec", JSON_BUILD_UNSIGNED(hr->not_after_usec)), + JSON_BUILD_PAIR_CONDITION(hr->password_change_now >= 0, "passwordChangeNow", JSON_BUILD_BOOLEAN(hr->password_change_now)), + JSON_BUILD_PAIR_CONDITION(hr->last_password_change_usec != UINT64_MAX, "lastPasswordChangeUSec", JSON_BUILD_UNSIGNED(hr->last_password_change_usec)), + JSON_BUILD_PAIR_CONDITION(hr->password_change_min_usec != UINT64_MAX, "passwordChangeMinUSec", JSON_BUILD_UNSIGNED(hr->password_change_min_usec)), + JSON_BUILD_PAIR_CONDITION(hr->password_change_max_usec != UINT64_MAX, "passwordChangeMaxUSec", JSON_BUILD_UNSIGNED(hr->password_change_max_usec)), + JSON_BUILD_PAIR_CONDITION(hr->password_change_warn_usec != UINT64_MAX, "passwordChangeWarnUSec", JSON_BUILD_UNSIGNED(hr->password_change_warn_usec)), + JSON_BUILD_PAIR_CONDITION(hr->password_change_inactive_usec != UINT64_MAX, "passwordChangeInactiveUSec", JSON_BUILD_UNSIGNED(hr->password_change_inactive_usec)))); + + if (r < 0) + return r; + + hr->mask = USER_RECORD_REGULAR | + (!strv_isempty(hr->hashed_password) ? USER_RECORD_PRIVILEGED : 0); + + *ret = TAKE_PTR(hr); + return 0; +} + +int nss_spwd_for_passwd(const struct passwd *pwd, struct spwd *ret_spwd, char **ret_buffer) { + size_t buflen = 4096; + int r; + + assert(pwd); + assert(ret_spwd); + assert(ret_buffer); + + for (;;) { + _cleanup_free_ char *buf = NULL; + struct spwd spwd, *result; + + buf = malloc(buflen); + if (!buf) + return -ENOMEM; + + r = getspnam_r(pwd->pw_name, &spwd, buf, buflen, &result); + if (r == 0) { + if (!result) + return -ESRCH; + + *ret_spwd = *result; + *ret_buffer = TAKE_PTR(buf); + return 0; + } + if (r < 0) + return -EIO; /* Weird, this should not return negative! */ + if (r != ERANGE) + return -r; + + if (buflen > SIZE_MAX / 2) + return -ERANGE; + + buflen *= 2; + buf = mfree(buf); + } +} + +int nss_user_record_by_name(const char *name, UserRecord **ret) { + _cleanup_free_ char *buf = NULL, *sbuf = NULL; + struct passwd pwd, *result; + bool incomplete = false; + size_t buflen = 4096; + struct spwd spwd; + int r; + + assert(name); + assert(ret); + + for (;;) { + buf = malloc(buflen); + if (!buf) + return -ENOMEM; + + r = getpwnam_r(name, &pwd, buf, buflen, &result); + if (r == 0) { + if (!result) + return -ESRCH; + + break; + } + + if (r < 0) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "getpwnam_r() returned a negative value"); + if (r != ERANGE) + return -r; + + if (buflen > SIZE_MAX / 2) + return -ERANGE; + + buflen *= 2; + buf = mfree(buf); + } + + r = nss_spwd_for_passwd(result, &spwd, &sbuf); + if (r < 0) { + log_debug_errno(r, "Failed to do shadow lookup for user %s, ignoring: %m", name); + incomplete = ERRNO_IS_PRIVILEGE(r); + } + + r = nss_passwd_to_user_record(result, r >= 0 ? &spwd : NULL, ret); + if (r < 0) + return r; + + (*ret)->incomplete = incomplete; + return 0; +} + +int nss_user_record_by_uid(uid_t uid, UserRecord **ret) { + _cleanup_free_ char *buf = NULL, *sbuf = NULL; + struct passwd pwd, *result; + bool incomplete = false; + size_t buflen = 4096; + struct spwd spwd; + int r; + + assert(ret); + + for (;;) { + buf = malloc(buflen); + if (!buf) + return -ENOMEM; + + r = getpwuid_r(uid, &pwd, buf, buflen, &result); + if (r == 0) { + if (!result) + return -ESRCH; + + break; + } + if (r < 0) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), "getpwuid_r() returned a negative value"); + if (r != ERANGE) + return -r; + + if (buflen > SIZE_MAX / 2) + return -ERANGE; + + buflen *= 2; + buf = mfree(buf); + } + + r = nss_spwd_for_passwd(result, &spwd, &sbuf); + if (r < 0) { + log_debug_errno(r, "Failed to do shadow lookup for UID " UID_FMT ", ignoring: %m", uid); + incomplete = ERRNO_IS_PRIVILEGE(r); + } + + r = nss_passwd_to_user_record(result, r >= 0 ? &spwd : NULL, ret); + if (r < 0) + return r; + + (*ret)->incomplete = incomplete; + return 0; +} diff --git a/src/shared/user-record-nss.h b/src/shared/user-record-nss.h new file mode 100644 index 0000000000..d5fb23ad2a --- /dev/null +++ b/src/shared/user-record-nss.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include +#include + +#include "user-record.h" + +/* Synthesizes a UserRecord object from NSS data */ + +int nss_passwd_to_user_record(const struct passwd *pwd, const struct spwd *spwd, UserRecord **ret); +int nss_spwd_for_passwd(const struct passwd *pwd, struct spwd *ret_spwd, char **ret_buffer); + +int nss_user_record_by_name(const char *name, UserRecord **ret); +int nss_user_record_by_uid(uid_t uid, UserRecord **ret); From ec8e4a0ef12ff2fd393e58c335602d605d94f846 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Aug 2019 15:26:32 +0200 Subject: [PATCH 06/29] shared: add internal API for querying JSON user records via varlink This new API can be used in place of NSS by our own internal code if more than the classic UNIX records are needed. --- src/shared/meson.build | 2 + src/shared/userdb.c | 1347 ++++++++++++++++++++++++++++++++++++++++ src/shared/userdb.h | 41 ++ 3 files changed, 1390 insertions(+) create mode 100644 src/shared/userdb.c create mode 100644 src/shared/userdb.h diff --git a/src/shared/meson.build b/src/shared/meson.build index d99aaa62c6..aaa0bcf794 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -197,6 +197,8 @@ shared_sources = files(''' user-record-nss.h user-record.c user-record.h + userdb.c + userdb.h utmp-wtmp.h varlink.c varlink.h diff --git a/src/shared/userdb.c b/src/shared/userdb.c new file mode 100644 index 0000000000..af2a1cc76f --- /dev/null +++ b/src/shared/userdb.c @@ -0,0 +1,1347 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "dirent-util.h" +#include "errno-util.h" +#include "fd-util.h" +#include "group-record-nss.h" +#include "missing_syscall.h" +#include "parse-util.h" +#include "set.h" +#include "socket-util.h" +#include "strv.h" +#include "user-record-nss.h" +#include "user-util.h" +#include "userdb.h" +#include "varlink.h" + +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(link_hash_ops, void, trivial_hash_func, trivial_compare_func, Varlink, varlink_unref); + +typedef enum LookupWhat { + LOOKUP_USER, + LOOKUP_GROUP, + LOOKUP_MEMBERSHIP, + _LOOKUP_WHAT_MAX, +} LookupWhat; + +struct UserDBIterator { + LookupWhat what; + Set *links; + bool nss_covered:1; + bool nss_iterating:1; + bool synthesize_root:1; + bool synthesize_nobody:1; + int error; + int nss_lock; + unsigned n_found; + sd_event *event; + UserRecord *found_user; /* when .what == LOOKUP_USER */ + GroupRecord *found_group; /* when .what == LOOKUP_GROUP */ + + char *found_user_name, *found_group_name; /* when .what == LOOKUP_MEMBERSHIP */ + char **members_of_group; + size_t index_members_of_group; + char *filter_user_name; +}; + +UserDBIterator* userdb_iterator_free(UserDBIterator *iterator) { + if (!iterator) + return NULL; + + set_free(iterator->links); + + switch (iterator->what) { + + case LOOKUP_USER: + user_record_unref(iterator->found_user); + + if (iterator->nss_iterating) + endpwent(); + + break; + + case LOOKUP_GROUP: + group_record_unref(iterator->found_group); + + if (iterator->nss_iterating) + endgrent(); + + break; + + case LOOKUP_MEMBERSHIP: + free(iterator->found_user_name); + free(iterator->found_group_name); + strv_free(iterator->members_of_group); + free(iterator->filter_user_name); + + if (iterator->nss_iterating) + endgrent(); + + break; + + default: + assert_not_reached("Unexpected state?"); + } + + sd_event_unref(iterator->event); + safe_close(iterator->nss_lock); + + return mfree(iterator); +} + +static UserDBIterator* userdb_iterator_new(LookupWhat what) { + UserDBIterator *i; + + assert(what >= 0); + assert(what < _LOOKUP_WHAT_MAX); + + i = new(UserDBIterator, 1); + if (!i) + return NULL; + + *i = (UserDBIterator) { + .what = what, + .nss_lock = -1, + }; + + return i; +} + +struct user_group_data { + JsonVariant *record; + bool incomplete; +}; + +static void user_group_data_release(struct user_group_data *d) { + json_variant_unref(d->record); +} + +static int userdb_on_query_reply( + Varlink *link, + JsonVariant *parameters, + const char *error_id, + VarlinkReplyFlags flags, + void *userdata) { + + UserDBIterator *iterator = userdata; + int r; + + assert(iterator); + + if (error_id) { + log_debug("Got lookup error: %s", error_id); + + if (STR_IN_SET(error_id, + "io.systemd.UserDatabase.NoRecordFound", + "io.systemd.UserDatabase.ConflictingRecordFound")) + r = -ESRCH; + else if (streq(error_id, "io.systemd.UserDatabase.ServiceNotAvailable")) + r = -EHOSTDOWN; + else if (streq(error_id, VARLINK_ERROR_TIMEOUT)) + r = -ETIMEDOUT; + else + r = -EIO; + + goto finish; + } + + switch (iterator->what) { + + case LOOKUP_USER: { + _cleanup_(user_group_data_release) struct user_group_data user_data = {}; + + static const JsonDispatch dispatch_table[] = { + { "record", _JSON_VARIANT_TYPE_INVALID, json_dispatch_variant, offsetof(struct user_group_data, record), 0 }, + { "incomplete", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(struct user_group_data, incomplete), 0 }, + {} + }; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + + assert_se(!iterator->found_user); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &user_data); + if (r < 0) + goto finish; + + if (!user_data.record) { + r = log_debug_errno(SYNTHETIC_ERRNO(EIO), "Reply is missing record key"); + goto finish; + } + + hr = user_record_new(); + if (!hr) { + r = -ENOMEM; + goto finish; + } + + r = user_record_load(hr, user_data.record, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_PERMISSIVE); + if (r < 0) + goto finish; + + if (!hr->service) { + r = log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "User record does not carry service information, refusing."); + goto finish; + } + + hr->incomplete = user_data.incomplete; + + /* We match the root user by the name since the name is our primary key. We match the nobody + * use by UID though, since the name might differ on OSes */ + if (streq_ptr(hr->user_name, "root")) + iterator->synthesize_root = false; + if (hr->uid == UID_NOBODY) + iterator->synthesize_nobody = false; + + iterator->found_user = TAKE_PTR(hr); + iterator->n_found++; + + /* More stuff coming? then let's just exit cleanly here */ + if (FLAGS_SET(flags, VARLINK_REPLY_CONTINUES)) + return 0; + + /* Otherwise, let's remove this link and exit cleanly then */ + r = 0; + goto finish; + } + + case LOOKUP_GROUP: { + _cleanup_(user_group_data_release) struct user_group_data group_data = {}; + + static const JsonDispatch dispatch_table[] = { + { "record", _JSON_VARIANT_TYPE_INVALID, json_dispatch_variant, offsetof(struct user_group_data, record), 0 }, + { "incomplete", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(struct user_group_data, incomplete), 0 }, + {} + }; + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; + + assert_se(!iterator->found_group); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &group_data); + if (r < 0) + goto finish; + + if (!group_data.record) { + r = log_debug_errno(SYNTHETIC_ERRNO(EIO), "Reply is missing record key"); + goto finish; + } + + g = group_record_new(); + if (!g) { + r = -ENOMEM; + goto finish; + } + + r = group_record_load(g, group_data.record, USER_RECORD_LOAD_REFUSE_SECRET|USER_RECORD_PERMISSIVE); + if (r < 0) + goto finish; + + if (!g->service) { + r = log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Group record does not carry service information, refusing."); + goto finish; + } + + g->incomplete = group_data.incomplete; + + if (streq_ptr(g->group_name, "root")) + iterator->synthesize_root = false; + if (g->gid == GID_NOBODY) + iterator->synthesize_nobody = false; + + iterator->found_group = TAKE_PTR(g); + iterator->n_found++; + + if (FLAGS_SET(flags, VARLINK_REPLY_CONTINUES)) + return 0; + + r = 0; + goto finish; + } + + case LOOKUP_MEMBERSHIP: { + struct membership_data { + const char *user_name; + const char *group_name; + } membership_data = {}; + + static const JsonDispatch dispatch_table[] = { + { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(struct membership_data, user_name), JSON_SAFE }, + { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(struct membership_data, group_name), JSON_SAFE }, + {} + }; + + assert(!iterator->found_user_name); + assert(!iterator->found_group_name); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &membership_data); + if (r < 0) + goto finish; + + iterator->found_user_name = mfree(iterator->found_user_name); + iterator->found_group_name = mfree(iterator->found_group_name); + + iterator->found_user_name = strdup(membership_data.user_name); + if (!iterator->found_user_name) { + r = -ENOMEM; + goto finish; + } + + iterator->found_group_name = strdup(membership_data.group_name); + if (!iterator->found_group_name) { + r = -ENOMEM; + goto finish; + } + + iterator->n_found++; + + if (FLAGS_SET(flags, VARLINK_REPLY_CONTINUES)) + return 0; + + r = 0; + goto finish; + } + + default: + assert_not_reached("unexpected lookup"); + } + +finish: + /* If we got one ESRCH, let that win. This way when we do a wild dump we won't be tripped up by bad + * errors if at least one connection ended cleanly */ + if (r == -ESRCH || iterator->error == 0) + iterator->error = -r; + + assert_se(set_remove(iterator->links, link) == link); + link = varlink_unref(link); + return 0; +} + +static int userdb_connect( + UserDBIterator *iterator, + const char *path, + const char *method, + bool more, + JsonVariant *query) { + + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + int r; + + assert(iterator); + assert(path); + assert(method); + + r = varlink_connect_address(&vl, path); + if (r < 0) + return log_debug_errno(r, "Unable to connect to %s: %m", path); + + varlink_set_userdata(vl, iterator); + + if (!iterator->event) { + r = sd_event_new(&iterator->event); + if (r < 0) + return log_debug_errno(r, "Unable to allocate event loop: %m"); + } + + r = varlink_attach_event(vl, iterator->event, SD_EVENT_PRIORITY_NORMAL); + if (r < 0) + return log_debug_errno(r, "Failed to attach varlink connection to event loop: %m"); + + (void) varlink_set_description(vl, path); + + r = varlink_bind_reply(vl, userdb_on_query_reply); + if (r < 0) + return log_debug_errno(r, "Failed to bind reply callback: %m"); + + if (more) + r = varlink_observe(vl, method, query); + else + r = varlink_invoke(vl, method, query); + if (r < 0) + return log_debug_errno(r, "Failed to invoke varlink method: %m"); + + r = set_ensure_allocated(&iterator->links, &link_hash_ops); + if (r < 0) + return log_debug_errno(r, "Failed to allocate set: %m"); + + r = set_put(iterator->links, vl); + if (r < 0) + return log_debug_errno(r, "Failed to add varlink connection to set: %m"); + + TAKE_PTR(vl); + return r; +} + +static int userdb_start_query( + UserDBIterator *iterator, + const char *method, + bool more, + JsonVariant *query, + UserDBFlags flags) { + + _cleanup_(strv_freep) char **except = NULL, **only = NULL; + _cleanup_(closedirp) DIR *d = NULL; + struct dirent *de; + const char *e; + int r, ret = 0; + + assert(iterator); + assert(method); + + e = getenv("SYSTEMD_BYPASS_USERDB"); + if (e) { + r = parse_boolean(e); + if (r > 0) + return -ENOLINK; + if (r < 0) { + except = strv_split(e, ":"); + if (!except) + return -ENOMEM; + } + } + + e = getenv("SYSTEMD_ONLY_USERDB"); + if (e) { + only = strv_split(e, ":"); + if (!only) + return -ENOMEM; + } + + /* First, let's talk to the multiplexer, if we can */ + if ((flags & (USERDB_AVOID_MULTIPLEXER|USERDB_AVOID_DYNAMIC_USER|USERDB_AVOID_NSS|USERDB_DONT_SYNTHESIZE)) == 0 && + !strv_contains(except, "io.systemd.Multiplexer") && + (!only || strv_contains(only, "io.systemd.Multiplexer"))) { + _cleanup_(json_variant_unrefp) JsonVariant *patched_query = json_variant_ref(query); + + r = json_variant_set_field_string(&patched_query, "service", "io.systemd.Multiplexer"); + if (r < 0) + return log_debug_errno(r, "Unable to set service JSON field: %m"); + + r = userdb_connect(iterator, "/run/systemd/userdb/io.systemd.Multiplexer", method, more, patched_query); + if (r >= 0) { + iterator->nss_covered = true; /* The multiplexer does NSS */ + return 0; + } + } + + d = opendir("/run/systemd/userdb/"); + if (!d) { + if (errno == ENOENT) + return -ESRCH; + + return -errno; + } + + FOREACH_DIRENT(de, d, return -errno) { + _cleanup_(json_variant_unrefp) JsonVariant *patched_query = NULL; + _cleanup_free_ char *p = NULL; + bool is_nss; + + if (streq(de->d_name, "io.systemd.Multiplexer")) /* We already tried this above, don't try this again */ + continue; + + if (FLAGS_SET(flags, USERDB_AVOID_DYNAMIC_USER) && + streq(de->d_name, "io.systemd.DynamicUser")) + continue; + + /* Avoid NSS is this is requested. Note that we also skip NSS when we were asked to skip the + * multiplexer, since in that case it's safer to do NSS in the client side emulation below + * (and when we run as part of systemd-userdbd.service we don't want to talk to ourselves + * anyway). */ + is_nss = streq(de->d_name, "io.systemd.NameServiceSwitch"); + if ((flags & (USERDB_AVOID_NSS|USERDB_AVOID_MULTIPLEXER)) && is_nss) + continue; + + if (strv_contains(except, de->d_name)) + continue; + + if (only && !strv_contains(only, de->d_name)) + continue; + + p = path_join("/run/systemd/userdb/", de->d_name); + if (!p) + return -ENOMEM; + + patched_query = json_variant_ref(query); + r = json_variant_set_field_string(&patched_query, "service", de->d_name); + if (r < 0) + return log_debug_errno(r, "Unable to set service JSON field: %m"); + + r = userdb_connect(iterator, p, method, more, patched_query); + if (is_nss && r >= 0) /* Turn off fallback NSS if we found the NSS service and could connect + * to it */ + iterator->nss_covered = true; + + if (ret == 0 && r < 0) + ret = r; + } + + if (set_isempty(iterator->links)) + return ret; /* propagate last error we saw if we couldn't connect to anything. */ + + /* We connected to some services, in this case, ignore the ones we failed on */ + return 0; +} + +static int userdb_process( + UserDBIterator *iterator, + UserRecord **ret_user_record, + GroupRecord **ret_group_record, + char **ret_user_name, + char **ret_group_name) { + + int r; + + assert(iterator); + + for (;;) { + if (iterator->what == LOOKUP_USER && iterator->found_user) { + if (ret_user_record) + *ret_user_record = TAKE_PTR(iterator->found_user); + else + iterator->found_user = user_record_unref(iterator->found_user); + + if (ret_group_record) + *ret_group_record = NULL; + if (ret_user_name) + *ret_user_name = NULL; + if (ret_group_name) + *ret_group_name = NULL; + + return 0; + } + + if (iterator->what == LOOKUP_GROUP && iterator->found_group) { + if (ret_group_record) + *ret_group_record = TAKE_PTR(iterator->found_group); + else + iterator->found_group = group_record_unref(iterator->found_group); + + if (ret_user_record) + *ret_user_record = NULL; + if (ret_user_name) + *ret_user_name = NULL; + if (ret_group_name) + *ret_group_name = NULL; + + return 0; + } + + if (iterator->what == LOOKUP_MEMBERSHIP && iterator->found_user_name && iterator->found_group_name) { + if (ret_user_name) + *ret_user_name = TAKE_PTR(iterator->found_user_name); + else + iterator->found_user_name = mfree(iterator->found_user_name); + + if (ret_group_name) + *ret_group_name = TAKE_PTR(iterator->found_group_name); + else + iterator->found_group_name = mfree(iterator->found_group_name); + + if (ret_user_record) + *ret_user_record = NULL; + if (ret_group_record) + *ret_group_record = NULL; + + return 0; + } + + if (set_isempty(iterator->links)) { + if (iterator->error == 0) + return -ESRCH; + + return -abs(iterator->error); + } + + if (!iterator->event) + return -ESRCH; + + r = sd_event_run(iterator->event, UINT64_MAX); + if (r < 0) + return r; + } +} + +static int synthetic_root_user_build(UserRecord **ret) { + return user_record_build( + ret, + JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING("root")), + JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(0)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(0)), + JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_STRING("/root")), + JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("intrinsic")))); +} + +static int synthetic_nobody_user_build(UserRecord **ret) { + return user_record_build( + ret, + JSON_BUILD_OBJECT(JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(NOBODY_USER_NAME)), + JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(UID_NOBODY)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(GID_NOBODY)), + JSON_BUILD_PAIR("shell", JSON_BUILD_STRING(NOLOGIN)), + JSON_BUILD_PAIR("locked", JSON_BUILD_BOOLEAN(true)), + JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("intrinsic")))); +} + +int userdb_by_name(const char *name, UserDBFlags flags, UserRecord **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *query = NULL; + int r; + + if (!valid_user_group_name(name)) + return -EINVAL; + + r = json_build(&query, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(name)))); + if (r < 0) + return r; + + iterator = userdb_iterator_new(LOOKUP_USER); + if (!iterator) + return -ENOMEM; + + r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", false, query, flags); + if (r >= 0) { + r = userdb_process(iterator, ret, NULL, NULL, NULL); + if (r >= 0) + return r; + } + + if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && !(iterator && iterator->nss_covered)) { + /* Make sure the NSS lookup doesn't recurse back to us. (EBUSY is fine here, it just means we + * already took the lock from our thread, which is totally OK.) */ + r = userdb_nss_compat_disable(); + if (r >= 0 || r == -EBUSY) { + iterator->nss_lock = r; + + /* Client-side NSS fallback */ + r = nss_user_record_by_name(name, ret); + if (r >= 0) + return r; + } + } + + if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) { + if (streq(name, "root")) + return synthetic_root_user_build(ret); + + if (streq(name, NOBODY_USER_NAME) && synthesize_nobody()) + return synthetic_nobody_user_build(ret); + } + + return r; +} + +int userdb_by_uid(uid_t uid, UserDBFlags flags, UserRecord **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *query = NULL; + int r; + + if (!uid_is_valid(uid)) + return -EINVAL; + + r = json_build(&query, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(uid)))); + if (r < 0) + return r; + + iterator = userdb_iterator_new(LOOKUP_USER); + if (!iterator) + return -ENOMEM; + + r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", false, query, flags); + if (r >= 0) { + r = userdb_process(iterator, ret, NULL, NULL, NULL); + if (r >= 0) + return r; + } + + if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && !(iterator && iterator->nss_covered)) { + r = userdb_nss_compat_disable(); + if (r >= 0 || r == -EBUSY) { + iterator->nss_lock = r; + + /* Client-side NSS fallback */ + r = nss_user_record_by_uid(uid, ret); + if (r >= 0) + return r; + } + } + + if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) { + if (uid == 0) + return synthetic_root_user_build(ret); + + if (uid == UID_NOBODY && synthesize_nobody()) + return synthetic_nobody_user_build(ret); + } + + return r; +} + +int userdb_all(UserDBFlags flags, UserDBIterator **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + int r; + + assert(ret); + + iterator = userdb_iterator_new(LOOKUP_USER); + if (!iterator) + return -ENOMEM; + + iterator->synthesize_root = iterator->synthesize_nobody = !FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE); + + r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetUserRecord", true, NULL, flags); + + if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && (r < 0 || !iterator->nss_covered)) { + iterator->nss_lock = userdb_nss_compat_disable(); + if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY) + return iterator->nss_lock; + + setpwent(); + iterator->nss_iterating = true; + goto finish; + } + + if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) + goto finish; + + return r; + +finish: + *ret = TAKE_PTR(iterator); + return 0; +} + +int userdb_iterator_get(UserDBIterator *iterator, UserRecord **ret) { + int r; + + assert(iterator); + assert(iterator->what == LOOKUP_USER); + + if (iterator->nss_iterating) { + struct passwd *pw; + + /* If NSS isn't covered elsewhere, let's iterate through it first, since it probably contains + * the more traditional sources, which are probably good to show first. */ + + pw = getpwent(); + if (pw) { + _cleanup_free_ char *buffer = NULL; + bool incomplete = false; + struct spwd spwd; + + if (streq_ptr(pw->pw_name, "root")) + iterator->synthesize_root = false; + if (pw->pw_uid == UID_NOBODY) + iterator->synthesize_nobody = false; + + r = nss_spwd_for_passwd(pw, &spwd, &buffer); + if (r < 0) { + log_debug_errno(r, "Failed to acquire shadow entry for user %s, ignoring: %m", pw->pw_name); + incomplete = ERRNO_IS_PRIVILEGE(r); + } + + r = nss_passwd_to_user_record(pw, r >= 0 ? &spwd : NULL, ret); + if (r < 0) + return r; + + if (ret) + (*ret)->incomplete = incomplete; + return r; + } + + if (errno != 0) + log_debug_errno(errno, "Failure to iterate NSS user database, ignoring: %m"); + + iterator->nss_iterating = false; + endpwent(); + } + + r = userdb_process(iterator, ret, NULL, NULL, NULL); + + if (r < 0) { + if (iterator->synthesize_root) { + iterator->synthesize_root = false; + iterator->n_found++; + return synthetic_root_user_build(ret); + } + + if (iterator->synthesize_nobody) { + iterator->synthesize_nobody = false; + iterator->n_found++; + return synthetic_nobody_user_build(ret); + } + } + + /* if we found at least one entry, then ignore errors and indicate that we reached the end */ + if (r < 0 && iterator->n_found > 0) + return -ESRCH; + + return r; +} + +static int synthetic_root_group_build(GroupRecord **ret) { + return group_record_build( + ret, + JSON_BUILD_OBJECT(JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING("root")), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(0)), + JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("intrinsic")))); +} + +static int synthetic_nobody_group_build(GroupRecord **ret) { + return group_record_build( + ret, + JSON_BUILD_OBJECT(JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(NOBODY_GROUP_NAME)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(GID_NOBODY)), + JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("intrinsic")))); +} + +int groupdb_by_name(const char *name, UserDBFlags flags, GroupRecord **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *query = NULL; + int r; + + if (!valid_user_group_name(name)) + return -EINVAL; + + r = json_build(&query, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(name)))); + if (r < 0) + return r; + + iterator = userdb_iterator_new(LOOKUP_GROUP); + if (!iterator) + return -ENOMEM; + + r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", false, query, flags); + if (r >= 0) { + r = userdb_process(iterator, NULL, ret, NULL, NULL); + if (r >= 0) + return r; + } + + if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && !(iterator && iterator->nss_covered)) { + r = userdb_nss_compat_disable(); + if (r >= 0 || r == -EBUSY) { + iterator->nss_lock = r; + + r = nss_group_record_by_name(name, ret); + if (r >= 0) + return r; + } + } + + if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) { + if (streq(name, "root")) + return synthetic_root_group_build(ret); + + if (streq(name, NOBODY_GROUP_NAME) && synthesize_nobody()) + return synthetic_nobody_group_build(ret); + } + + return r; +} + +int groupdb_by_gid(gid_t gid, UserDBFlags flags, GroupRecord **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *query = NULL; + int r; + + if (!gid_is_valid(gid)) + return -EINVAL; + + r = json_build(&query, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(gid)))); + if (r < 0) + return r; + + iterator = userdb_iterator_new(LOOKUP_GROUP); + if (!iterator) + return -ENOMEM; + + r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", false, query, flags); + if (r >= 0) { + r = userdb_process(iterator, NULL, ret, NULL, NULL); + if (r >= 0) + return r; + } + + if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && !(iterator && iterator->nss_covered)) { + r = userdb_nss_compat_disable(); + if (r >= 0 || r == -EBUSY) { + iterator->nss_lock = r; + + r = nss_group_record_by_gid(gid, ret); + if (r >= 0) + return r; + } + } + + if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) { + if (gid == 0) + return synthetic_root_group_build(ret); + + if (gid == GID_NOBODY && synthesize_nobody()) + return synthetic_nobody_group_build(ret); + } + + return r; +} + +int groupdb_all(UserDBFlags flags, UserDBIterator **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + int r; + + assert(ret); + + iterator = userdb_iterator_new(LOOKUP_GROUP); + if (!iterator) + return -ENOMEM; + + iterator->synthesize_root = iterator->synthesize_nobody = !FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE); + + r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetGroupRecord", true, NULL, flags); + + if (!FLAGS_SET(flags, USERDB_AVOID_NSS) && (r < 0 || !iterator->nss_covered)) { + iterator->nss_lock = userdb_nss_compat_disable(); + if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY) + return iterator->nss_lock; + + setgrent(); + iterator->nss_iterating = true; + goto finish; + } + + if (!FLAGS_SET(flags, USERDB_DONT_SYNTHESIZE)) + goto finish; + + return r; + +finish: + *ret = TAKE_PTR(iterator); + return 0; +} + +int groupdb_iterator_get(UserDBIterator *iterator, GroupRecord **ret) { + int r; + + assert(iterator); + assert(iterator->what == LOOKUP_GROUP); + + if (iterator->nss_iterating) { + struct group *gr; + + errno = 0; + gr = getgrent(); + if (gr) { + _cleanup_free_ char *buffer = NULL; + bool incomplete = false; + struct sgrp sgrp; + + if (streq_ptr(gr->gr_name, "root")) + iterator->synthesize_root = false; + if (gr->gr_gid == GID_NOBODY) + iterator->synthesize_nobody = false; + + r = nss_sgrp_for_group(gr, &sgrp, &buffer); + if (r < 0) { + log_debug_errno(r, "Failed to acquire shadow entry for group %s, ignoring: %m", gr->gr_name); + incomplete = ERRNO_IS_PRIVILEGE(r); + } + + r = nss_group_to_group_record(gr, r >= 0 ? &sgrp : NULL, ret); + if (r < 0) + return r; + + if (ret) + (*ret)->incomplete = incomplete; + return r; + } + + if (errno != 0) + log_debug_errno(errno, "Failure to iterate NSS group database, ignoring: %m"); + + iterator->nss_iterating = false; + endgrent(); + } + + r = userdb_process(iterator, NULL, ret, NULL, NULL); + if (r < 0) { + if (iterator->synthesize_root) { + iterator->synthesize_root = false; + iterator->n_found++; + return synthetic_root_group_build(ret); + } + + if (iterator->synthesize_nobody) { + iterator->synthesize_nobody = false; + iterator->n_found++; + return synthetic_nobody_group_build(ret); + } + } + + /* if we found at least one entry, then ignore errors and indicate that we reached the end */ + if (r < 0 && iterator->n_found > 0) + return -ESRCH; + + return r; +} + +int membershipdb_by_user(const char *name, UserDBFlags flags, UserDBIterator **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *query = NULL; + int r; + + assert(ret); + + if (!valid_user_group_name(name)) + return -EINVAL; + + r = json_build(&query, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(name)))); + if (r < 0) + return r; + + iterator = userdb_iterator_new(LOOKUP_MEMBERSHIP); + if (!iterator) + return -ENOMEM; + + r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetMemberships", true, query, flags); + if ((r >= 0 && iterator->nss_covered) || FLAGS_SET(flags, USERDB_AVOID_NSS)) + goto finish; + + iterator->nss_lock = userdb_nss_compat_disable(); + if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY) + return iterator->nss_lock; + + iterator->filter_user_name = strdup(name); + if (!iterator->filter_user_name) + return -ENOMEM; + + setgrent(); + iterator->nss_iterating = true; + + r = 0; + +finish: + if (r >= 0) + *ret = TAKE_PTR(iterator); + return r; +} + +int membershipdb_by_group(const char *name, UserDBFlags flags, UserDBIterator **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *query = NULL; + _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; + int r; + + assert(ret); + + if (!valid_user_group_name(name)) + return -EINVAL; + + r = json_build(&query, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(name)))); + if (r < 0) + return r; + + iterator = userdb_iterator_new(LOOKUP_MEMBERSHIP); + if (!iterator) + return -ENOMEM; + + r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetMemberships", true, query, flags); + if ((r >= 0 && iterator->nss_covered) || FLAGS_SET(flags, USERDB_AVOID_NSS)) + goto finish; + + iterator->nss_lock = userdb_nss_compat_disable(); + if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY) + return iterator->nss_lock; + + /* We ignore all errors here, since the group might be defined by a userdb native service, and we queried them already above. */ + (void) nss_group_record_by_name(name, &gr); + if (gr) { + iterator->members_of_group = strv_copy(gr->members); + if (!iterator->members_of_group) + return -ENOMEM; + + iterator->index_members_of_group = 0; + + iterator->found_group_name = strdup(name); + if (!iterator->found_group_name) + return -ENOMEM; + } + + r = 0; + +finish: + if (r >= 0) + *ret = TAKE_PTR(iterator); + + return r; +} + +int membershipdb_all(UserDBFlags flags, UserDBIterator **ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + int r; + + assert(ret); + + iterator = userdb_iterator_new(LOOKUP_MEMBERSHIP); + if (!iterator) + return -ENOMEM; + + r = userdb_start_query(iterator, "io.systemd.UserDatabase.GetMemberships", true, NULL, flags); + if ((r >= 0 && iterator->nss_covered) || FLAGS_SET(flags, USERDB_AVOID_NSS)) + goto finish; + + iterator->nss_lock = userdb_nss_compat_disable(); + if (iterator->nss_lock < 0 && iterator->nss_lock != -EBUSY) + return iterator->nss_lock; + + setgrent(); + iterator->nss_iterating = true; + + r = 0; + +finish: + if (r >= 0) + *ret = TAKE_PTR(iterator); + + return r; +} + +int membershipdb_iterator_get( + UserDBIterator *iterator, + char **ret_user, + char **ret_group) { + + int r; + + assert(iterator); + + for (;;) { + /* If we are iteratring through NSS acquire a new group entry if we haven't acquired one yet. */ + if (!iterator->members_of_group) { + struct group *g; + + if (!iterator->nss_iterating) + break; + + assert(!iterator->found_user_name); + do { + errno = 0; + g = getgrent(); + if (!g) { + if (errno != 0) + log_debug_errno(errno, "Failure during NSS group iteration, ignoring: %m"); + break; + } + + } while (iterator->filter_user_name ? !strv_contains(g->gr_mem, iterator->filter_user_name) : + strv_isempty(g->gr_mem)); + + if (g) { + r = free_and_strdup(&iterator->found_group_name, g->gr_name); + if (r < 0) + return r; + + if (iterator->filter_user_name) + iterator->members_of_group = strv_new(iterator->filter_user_name); + else + iterator->members_of_group = strv_copy(g->gr_mem); + if (!iterator->members_of_group) + return -ENOMEM; + + iterator->index_members_of_group = 0; + } else { + iterator->nss_iterating = false; + endgrent(); + break; + } + } + + assert(iterator->found_group_name); + assert(iterator->members_of_group); + assert(!iterator->found_user_name); + + if (iterator->members_of_group[iterator->index_members_of_group]) { + _cleanup_free_ char *cu = NULL, *cg = NULL; + + if (ret_user) { + cu = strdup(iterator->members_of_group[iterator->index_members_of_group]); + if (!cu) + return -ENOMEM; + } + + if (ret_group) { + cg = strdup(iterator->found_group_name); + if (!cg) + return -ENOMEM; + } + + if (ret_user) + *ret_user = TAKE_PTR(cu); + + if (ret_group) + *ret_group = TAKE_PTR(cg); + + iterator->index_members_of_group++; + return 0; + } + + iterator->members_of_group = strv_free(iterator->members_of_group); + iterator->found_group_name = mfree(iterator->found_group_name); + } + + r = userdb_process(iterator, NULL, NULL, ret_user, ret_group); + if (r < 0 && iterator->n_found > 0) + return -ESRCH; + + return r; +} + +int membershipdb_by_group_strv(const char *name, UserDBFlags flags, char ***ret) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_strv_free_ char **members = NULL; + int r; + + assert(name); + assert(ret); + + r = membershipdb_by_group(name, flags, &iterator); + if (r < 0) + return r; + + for (;;) { + _cleanup_free_ char *user_name = NULL; + + r = membershipdb_iterator_get(iterator, &user_name, NULL); + if (r == -ESRCH) + break; + if (r < 0) + return r; + + r = strv_consume(&members, TAKE_PTR(user_name)); + if (r < 0) + return r; + } + + strv_sort(members); + strv_uniq(members); + + *ret = TAKE_PTR(members); + return 0; +} + +static int userdb_thread_sockaddr(struct sockaddr_un *ret_sa, socklen_t *ret_salen) { + static const uint8_t + k1[16] = { 0x35, 0xc1, 0x1f, 0x41, 0x59, 0xc6, 0xa0, 0xf9, 0x33, 0x4b, 0x17, 0x3d, 0xb9, 0xf6, 0x14, 0xd9 }, + k2[16] = { 0x6a, 0x11, 0x4c, 0x37, 0xe5, 0xa3, 0x8c, 0xa6, 0x93, 0x55, 0x64, 0x8c, 0x93, 0xee, 0xa1, 0x7b }; + + struct siphash sh; + uint64_t x, y; + pid_t tid; + void *p; + + assert(ret_sa); + assert(ret_salen); + + /* This calculates an AF_UNIX socket address in the abstract namespace whose existance works as an + * indicator whether to emulate NSS records for complex user records that are also available via the + * varlink protocol. The name of the socket is picked in a way so that: + * + * → it is per-thread (by hashing from the TID) + * + * → is not guessable for foreign processes (by hashing from the — hopefully secret — AT_RANDOM + * value every process gets passed from the kernel + * + * By using a socket the NSS emulation can be nicely turned off for limited amounts of time only, + * simply controlled by the lifetime of the fd itself. By using an AF_UNIX socket in the abstract + * namespace the lock is automatically cleaned up when the process dies abnormally. + * + */ + + p = ULONG_TO_PTR(getauxval(AT_RANDOM)); + if (!p) + return -EIO; + + tid = gettid(); + + siphash24_init(&sh, k1); + siphash24_compress(p, 16, &sh); + siphash24_compress(&tid, sizeof(tid), &sh); + x = siphash24_finalize(&sh); + + siphash24_init(&sh, k2); + siphash24_compress(p, 16, &sh); + siphash24_compress(&tid, sizeof(tid), &sh); + y = siphash24_finalize(&sh); + + *ret_sa = (struct sockaddr_un) { + .sun_family = AF_UNIX, + }; + + sprintf(ret_sa->sun_path + 1, "userdb-%016" PRIx64 "%016" PRIx64, x, y); + *ret_salen = offsetof(struct sockaddr_un, sun_path) + 1 + 7 + 32; + + return 0; +} + +int userdb_nss_compat_is_enabled(void) { + _cleanup_close_ int fd = -1; + union sockaddr_union sa; + socklen_t salen; + int r; + + /* Tests whether the NSS compatibility logic is currently turned on for the invoking thread. Returns + * true if NSS compatibility is turned on, i.e. whether NSS records shall be synthesized from complex + * user records. */ + + r = userdb_thread_sockaddr(&sa.un, &salen); + if (r < 0) + return r; + + fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0); + if (fd < 0) + return -errno; + + /* Try to connect(). This doesn't do anything really, except that it checks whether the socket + * address is bound at all. */ + if (connect(fd, &sa.sa, salen) < 0) { + if (errno == ECONNREFUSED) /* the socket is not bound, hence NSS emulation shall be done */ + return true; + + return -errno; + } + + return false; +} + +int userdb_nss_compat_disable(void) { + _cleanup_close_ int fd = -1; + union sockaddr_union sa; + socklen_t salen; + int r; + + /* Turn off the NSS compatibility logic for the invoking thread. By default NSS records are + * synthesized for all complex user records looked up via NSS. If this call is invoked this is + * disabled for the invoking thread, but only for it. A caller that natively supports the varlink + * user record protocol may use that to turn off the compatibility for NSS lookups. */ + + r = userdb_thread_sockaddr(&sa.un, &salen); + if (r < 0) + return r; + + fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + if (fd < 0) + return -errno; + + if (bind(fd, &sa.sa, salen) < 0) { + if (errno == EADDRINUSE) /* lock already taken, convert this into a recognizable error */ + return -EBUSY; + + return -errno; + } + + return TAKE_FD(fd); +} diff --git a/src/shared/userdb.h b/src/shared/userdb.h new file mode 100644 index 0000000000..4288b0ff95 --- /dev/null +++ b/src/shared/userdb.h @@ -0,0 +1,41 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include +#include + +#include "group-record.h" +#include "user-record.h" + +/* Inquire local services for user/group records */ + +typedef struct UserDBIterator UserDBIterator; + +UserDBIterator *userdb_iterator_free(UserDBIterator *iterator); +DEFINE_TRIVIAL_CLEANUP_FUNC(UserDBIterator*, userdb_iterator_free); + +typedef enum UserDBFlags { + USERDB_AVOID_NSS = 1 << 0, /* don't do client-side nor server-side NSS */ + USERDB_AVOID_DYNAMIC_USER = 1 << 1, /* exclude looking up in io.systemd.DynamicUser */ + USERDB_AVOID_MULTIPLEXER = 1 << 2, /* exclude looking up via io.systemd.Multiplexer */ + USERDB_DONT_SYNTHESIZE = 1 << 3, /* don't synthesize root/nobody */ +} UserDBFlags; + +int userdb_by_name(const char *name, UserDBFlags flags, UserRecord **ret); +int userdb_by_uid(uid_t uid, UserDBFlags flags, UserRecord **ret); +int userdb_all(UserDBFlags flags, UserDBIterator **ret); +int userdb_iterator_get(UserDBIterator *iterator, UserRecord **ret); + +int groupdb_by_name(const char *name, UserDBFlags flags, GroupRecord **ret); +int groupdb_by_gid(gid_t gid, UserDBFlags flags, GroupRecord **ret); +int groupdb_all(UserDBFlags flags, UserDBIterator **ret); +int groupdb_iterator_get(UserDBIterator *iterator, GroupRecord **ret); + +int membershipdb_by_user(const char *name, UserDBFlags flags, UserDBIterator **ret); +int membershipdb_by_group(const char *name, UserDBFlags flags, UserDBIterator **ret); +int membershipdb_all(UserDBFlags flags, UserDBIterator **ret); +int membershipdb_iterator_get(UserDBIterator *iterator, char **user, char **group); +int membershipdb_by_group_strv(const char *name, UserDBFlags flags, char ***ret); + +int userdb_nss_compat_is_enabled(void); +int userdb_nss_compat_disable(void); From 295c1a6e456936960d7190f94ff0c1eb51102f33 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 5 Aug 2019 18:21:49 +0200 Subject: [PATCH 07/29] shared: add helpers for displaying new-style user/group records to users --- src/shared/group-record-show.c | 76 ++++++ src/shared/group-record-show.h | 6 + src/shared/meson.build | 4 + src/shared/user-record-show.c | 466 +++++++++++++++++++++++++++++++++ src/shared/user-record-show.h | 8 + 5 files changed, 560 insertions(+) create mode 100644 src/shared/group-record-show.c create mode 100644 src/shared/group-record-show.h create mode 100644 src/shared/user-record-show.c create mode 100644 src/shared/user-record-show.h diff --git a/src/shared/group-record-show.c b/src/shared/group-record-show.c new file mode 100644 index 0000000000..d0300e483c --- /dev/null +++ b/src/shared/group-record-show.c @@ -0,0 +1,76 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "format-util.h" +#include "group-record-show.h" +#include "strv.h" +#include "user-util.h" +#include "userdb.h" + +void group_record_show(GroupRecord *gr, bool show_full_user_info) { + int r; + + printf(" Group name: %s\n", + group_record_group_name_and_realm(gr)); + + printf(" Disposition: %s\n", user_disposition_to_string(group_record_disposition(gr))); + + if (gr->last_change_usec != USEC_INFINITY) { + char buf[FORMAT_TIMESTAMP_MAX]; + printf(" Last Change: %s\n", format_timestamp(buf, sizeof(buf), gr->last_change_usec)); + } + + if (gid_is_valid(gr->gid)) + printf(" GID: " GID_FMT "\n", gr->gid); + + if (show_full_user_info) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + + r = membershipdb_by_group(gr->group_name, 0, &iterator); + if (r < 0) { + errno = -r; + printf(" Members: (can't acquire: %m)"); + } else { + const char *prefix = " Members:"; + + for (;;) { + _cleanup_free_ char *user = NULL; + + r = membershipdb_iterator_get(iterator, &user, NULL); + if (r == -ESRCH) + break; + if (r < 0) { + errno = -r; + printf("%s (can't iterate: %m\n", prefix); + break; + } + + printf("%s %s\n", prefix, user); + prefix = " "; + } + } + } else { + const char *prefix = " Members:"; + char **i; + + STRV_FOREACH(i, gr->members) { + printf("%s %s\n", prefix, *i); + prefix = " "; + } + } + + if (!strv_isempty(gr->administrators)) { + const char *prefix = " Admins:"; + char **i; + + STRV_FOREACH(i, gr->administrators) { + printf("%s %s\n", prefix, *i); + prefix = " "; + } + } + + if (!strv_isempty(gr->hashed_password)) + printf(" Passwords: %zu\n", strv_length(gr->hashed_password)); + + if (gr->service) + printf(" Service: %s\n", gr->service); +} diff --git a/src/shared/group-record-show.h b/src/shared/group-record-show.h new file mode 100644 index 0000000000..12bdbd1724 --- /dev/null +++ b/src/shared/group-record-show.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "group-record.h" + +void group_record_show(GroupRecord *gr, bool show_full_user_info); diff --git a/src/shared/meson.build b/src/shared/meson.build index aaa0bcf794..4d98fb2c17 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -88,6 +88,8 @@ shared_sources = files(''' 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 @@ -195,6 +197,8 @@ shared_sources = files(''' 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 diff --git a/src/shared/user-record-show.c b/src/shared/user-record-show.c new file mode 100644 index 0000000000..e5eff50840 --- /dev/null +++ b/src/shared/user-record-show.c @@ -0,0 +1,466 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "format-util.h" +#include "fs-util.h" +#include "group-record.h" +#include "process-util.h" +#include "rlimit-util.h" +#include "strv.h" +#include "terminal-util.h" +#include "user-record-show.h" +#include "user-util.h" +#include "userdb.h" + +const char *user_record_state_color(const char *state) { + if (STR_IN_SET(state, "unfixated", "absent")) + return ansi_grey(); + else if (streq(state, "active")) + return ansi_highlight_green(); + else if (streq(state, "locked")) + return ansi_highlight_yellow(); + + return NULL; +} + +void user_record_show(UserRecord *hr, bool show_full_group_info) { + const char *hd, *ip, *shell; + UserStorage storage; + usec_t t; + size_t k; + int r, b; + + printf(" User name: %s\n", + user_record_user_name_and_realm(hr)); + + if (hr->state) { + const char *color; + + color = user_record_state_color(hr->state); + + printf(" State: %s%s%s\n", + strempty(color), hr->state, color ? ansi_normal() : ""); + } + + printf(" Disposition: %s\n", user_disposition_to_string(user_record_disposition(hr))); + + if (hr->last_change_usec != USEC_INFINITY) { + char buf[FORMAT_TIMESTAMP_MAX]; + printf(" Last Change: %s\n", format_timestamp(buf, sizeof(buf), hr->last_change_usec)); + } + + if (hr->last_password_change_usec != USEC_INFINITY && + hr->last_password_change_usec != hr->last_change_usec) { + char buf[FORMAT_TIMESTAMP_MAX]; + printf(" Last Passw.: %s\n", format_timestamp(buf, sizeof(buf), hr->last_password_change_usec)); + } + + r = user_record_test_blocked(hr); + switch (r) { + + case -ESTALE: + printf(" Login OK: %sno%s (last change time is in the future)\n", ansi_highlight_red(), ansi_normal()); + break; + + case -ENOLCK: + printf(" Login OK: %sno%s (record is locked)\n", ansi_highlight_red(), ansi_normal()); + break; + + case -EL2HLT: + printf(" Login OK: %sno%s (record not valid yet))\n", ansi_highlight_red(), ansi_normal()); + break; + + case -EL3HLT: + printf(" Login OK: %sno%s (record not valid anymore))\n", ansi_highlight_red(), ansi_normal()); + break; + + default: { + usec_t y; + + if (r < 0) { + errno = -r; + printf(" Login OK: %sno%s (%m)\n", ansi_highlight_red(), ansi_normal()); + break; + } + + if (is_nologin_shell(user_record_shell(hr))) { + printf(" Login OK: %sno%s (nologin shell)\n", ansi_highlight_red(), ansi_normal()); + break; + } + + y = user_record_ratelimit_next_try(hr); + if (y != USEC_INFINITY && y > now(CLOCK_REALTIME)) { + printf(" Login OK: %sno%s (ratelimit)\n", ansi_highlight_red(), ansi_normal()); + break; + } + + printf(" Login OK: %syes%s\n", ansi_highlight_green(), ansi_normal()); + break; + }} + + r = user_record_test_password_change_required(hr); + switch (r) { + + case -EKEYREVOKED: + printf(" Password OK: %schange now%s\n", ansi_highlight_yellow(), ansi_normal()); + break; + + case -EOWNERDEAD: + printf(" Password OK: %sexpired%s (change now!)\n", ansi_highlight_yellow(), ansi_normal()); + break; + + case -EKEYREJECTED: + printf(" Password OK: %sexpired%s (for good)\n", ansi_highlight_red(), ansi_normal()); + break; + + case -EKEYEXPIRED: + printf(" Password OK: %sexpires soon%s\n", ansi_highlight_yellow(), ansi_normal()); + break; + + case -ENETDOWN: + printf(" Password OK: %sno timestamp%s\n", ansi_highlight_red(), ansi_normal()); + break; + + case -EROFS: + printf(" Password OK: %schange not permitted%s\n", ansi_highlight_yellow(), ansi_normal()); + break; + + default: + if (r < 0) { + errno = -r; + printf(" Password OK: %sno%s (%m)\n", ansi_highlight_yellow(), ansi_normal()); + break; + } + + printf(" Password OK: %syes%s\n", ansi_highlight_green(), ansi_normal()); + break; + } + + if (uid_is_valid(hr->uid)) + printf(" UID: " UID_FMT "\n", hr->uid); + if (gid_is_valid(hr->gid)) { + if (show_full_group_info) { + _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; + + r = groupdb_by_gid(hr->gid, 0, &gr); + if (r < 0) { + errno = -r; + printf(" GID: " GID_FMT " (unresolvable: %m)\n", hr->gid); + } else + printf(" GID: " GID_FMT " (%s)\n", hr->gid, gr->group_name); + } else + printf(" GID: " GID_FMT "\n", hr->gid); + } else if (uid_is_valid(hr->uid)) /* Show UID as GID if not separately configured */ + printf(" GID: " GID_FMT "\n", (gid_t) hr->uid); + + if (show_full_group_info) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + + r = membershipdb_by_user(hr->user_name, 0, &iterator); + if (r < 0) { + errno = -r; + printf(" Aux. Groups: (can't acquire: %m)\n"); + } else { + const char *prefix = " Aux. Groups:"; + + for (;;) { + _cleanup_free_ char *group = NULL; + + r = membershipdb_iterator_get(iterator, NULL, &group); + if (r == -ESRCH) + break; + if (r < 0) { + errno = -r; + printf("%s (can't iterate: %m)\n", prefix); + break; + } + + printf("%s %s\n", prefix, group); + prefix = " "; + } + } + } + + if (hr->real_name && !streq(hr->real_name, hr->user_name)) + printf(" Real Name: %s\n", hr->real_name); + + hd = user_record_home_directory(hr); + if (hd) + printf(" Directory: %s\n", hd); + + storage = user_record_storage(hr); + if (storage >= 0) /* Let's be political, and clarify which storage we like, and which we don't. About CIFS we don't complain. */ + printf(" Storage: %s%s\n", user_storage_to_string(storage), + storage == USER_LUKS ? " (strong encryption)" : + storage == USER_FSCRYPT ? " (weak encryption)" : + IN_SET(storage, USER_DIRECTORY, USER_SUBVOLUME) ? " (no encryption)" : ""); + + ip = user_record_image_path(hr); + if (ip && !streq_ptr(ip, hd)) + printf(" Image Path: %s\n", ip); + + b = user_record_removable(hr); + if (b >= 0) + printf(" Removable: %s\n", yes_no(b)); + + shell = user_record_shell(hr); + if (shell) + printf(" Shell: %s\n", shell); + + if (hr->email_address) + printf(" Email: %s\n", hr->email_address); + if (hr->location) + printf(" Location: %s\n", hr->location); + if (hr->password_hint) + printf(" Passw. Hint: %s\n", hr->password_hint); + if (hr->icon_name) + printf(" Icon Name: %s\n", hr->icon_name); + + if (hr->time_zone) + printf(" Time Zone: %s\n", hr->time_zone); + + if (hr->preferred_language) + printf(" Language: %s\n", hr->preferred_language); + + if (!strv_isempty(hr->environment)) { + char **i; + + STRV_FOREACH(i, hr->environment) { + printf(i == hr->environment ? + " Environment: %s\n" : + " %s\n", *i); + } + } + + if (hr->locked >= 0) + printf(" Locked: %s\n", yes_no(hr->locked)); + + if (hr->not_before_usec != UINT64_MAX) { + char buf[FORMAT_TIMESTAMP_MAX]; + printf(" Not Before: %s\n", format_timestamp(buf, sizeof(buf), hr->not_before_usec)); + } + + if (hr->not_after_usec != UINT64_MAX) { + char buf[FORMAT_TIMESTAMP_MAX]; + printf(" Not After: %s\n", format_timestamp(buf, sizeof(buf), hr->not_after_usec)); + } + + if (hr->umask != MODE_INVALID) + printf(" UMask: 0%03o\n", hr->umask); + + if (nice_is_valid(hr->nice_level)) + printf(" Nice: %i\n", hr->nice_level); + + for (int j = 0; j < _RLIMIT_MAX; j++) { + if (hr->rlimits[j]) + printf(" Limit: RLIMIT_%s=%" PRIu64 ":%" PRIu64 "\n", + rlimit_to_string(j), (uint64_t) hr->rlimits[j]->rlim_cur, (uint64_t) hr->rlimits[j]->rlim_max); + } + + if (hr->tasks_max != UINT64_MAX) + printf(" Tasks Max: %" PRIu64 "\n", hr->tasks_max); + + if (hr->memory_high != UINT64_MAX) { + char buf[FORMAT_BYTES_MAX]; + printf(" Memory High: %s\n", format_bytes(buf, sizeof(buf), hr->memory_high)); + } + + if (hr->memory_max != UINT64_MAX) { + char buf[FORMAT_BYTES_MAX]; + printf(" Memory Max: %s\n", format_bytes(buf, sizeof(buf), hr->memory_max)); + } + + if (hr->cpu_weight != UINT64_MAX) + printf(" CPU Weight: %" PRIu64 "\n", hr->cpu_weight); + + if (hr->io_weight != UINT64_MAX) + printf(" IO Weight: %" PRIu64 "\n", hr->io_weight); + + if (hr->access_mode != MODE_INVALID) + printf(" Access Mode: 0%03oo\n", user_record_access_mode(hr)); + + if (storage == USER_LUKS) { + printf("LUKS Discard: %s\n", yes_no(user_record_luks_discard(hr))); + + if (!sd_id128_is_null(hr->luks_uuid)) + printf(" LUKS UUID: " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(hr->luks_uuid)); + if (!sd_id128_is_null(hr->partition_uuid)) + printf(" Part UUID: " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(hr->partition_uuid)); + if (!sd_id128_is_null(hr->file_system_uuid)) + printf(" FS UUID: " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(hr->file_system_uuid)); + + if (hr->file_system_type) + printf(" File System: %s\n", user_record_file_system_type(hr)); + + if (hr->luks_cipher) + printf(" LUKS Cipher: %s\n", hr->luks_cipher); + if (hr->luks_cipher_mode) + printf(" Cipher Mode: %s\n", hr->luks_cipher_mode); + if (hr->luks_volume_key_size != UINT64_MAX) + printf(" Volume Key: %" PRIu64 "bit\n", hr->luks_volume_key_size * 8); + + if (hr->luks_pbkdf_type) + printf(" PBKDF Type: %s\n", hr->luks_pbkdf_type); + if (hr->luks_pbkdf_hash_algorithm) + printf(" PBKDF Hash: %s\n", hr->luks_pbkdf_hash_algorithm); + if (hr->luks_pbkdf_time_cost_usec != UINT64_MAX) { + char buf[FORMAT_TIMESPAN_MAX]; + printf(" PBKDF Time: %s\n", format_timespan(buf, sizeof(buf), hr->luks_pbkdf_time_cost_usec, 0)); + } + if (hr->luks_pbkdf_memory_cost != UINT64_MAX) { + char buf[FORMAT_BYTES_MAX]; + printf(" PBKDF Bytes: %s\n", format_bytes(buf, sizeof(buf), hr->luks_pbkdf_memory_cost)); + } + if (hr->luks_pbkdf_parallel_threads != UINT64_MAX) + printf("PBKDF Thread: %" PRIu64 "\n", hr->luks_pbkdf_parallel_threads); + + } else if (storage == USER_CIFS) { + + if (hr->cifs_service) + printf("CIFS Service: %s\n", hr->cifs_service); + } + + if (hr->cifs_user_name) + printf(" CIFS User: %s\n", user_record_cifs_user_name(hr)); + if (hr->cifs_domain) + printf(" CIFS Domain: %s\n", hr->cifs_domain); + + if (storage != USER_CLASSIC) + printf(" Mount Flags: %s %s %s\n", + hr->nosuid ? "nosuid" : "suid", + hr->nodev ? "nodev" : "dev", + hr->noexec ? "noexec" : "exec"); + + if (hr->skeleton_directory) + printf(" Skel. Dir.: %s\n", user_record_skeleton_directory(hr)); + + if (hr->disk_size != UINT64_MAX) { + char buf[FORMAT_BYTES_MAX]; + printf(" Disk Size: %s\n", format_bytes(buf, sizeof(buf), hr->disk_size)); + } + + if (hr->disk_usage != UINT64_MAX) { + char buf[FORMAT_BYTES_MAX]; + printf(" Disk Usage: %s\n", format_bytes(buf, sizeof(buf), hr->disk_usage)); + } + + if (hr->disk_free != UINT64_MAX) { + char buf[FORMAT_BYTES_MAX]; + printf(" Disk Free: %s\n", format_bytes(buf, sizeof(buf), hr->disk_free)); + } + + if (hr->disk_floor != UINT64_MAX) { + char buf[FORMAT_BYTES_MAX]; + printf(" Disk Floor: %s\n", format_bytes(buf, sizeof(buf), hr->disk_floor)); + } + + if (hr->disk_ceiling != UINT64_MAX) { + char buf[FORMAT_BYTES_MAX]; + printf("Disk Ceiling: %s\n", format_bytes(buf, sizeof(buf), hr->disk_ceiling)); + } + + if (hr->good_authentication_counter != UINT64_MAX) + printf(" Good Auth.: %" PRIu64 "\n", hr->good_authentication_counter); + + if (hr->last_good_authentication_usec != UINT64_MAX) { + char buf[FORMAT_TIMESTAMP_MAX]; + printf(" Last Good: %s\n", format_timestamp(buf, sizeof(buf), hr->last_good_authentication_usec)); + } + + if (hr->bad_authentication_counter != UINT64_MAX) + printf(" Bad Auth.: %" PRIu64 "\n", hr->bad_authentication_counter); + + if (hr->last_bad_authentication_usec != UINT64_MAX) { + char buf[FORMAT_TIMESTAMP_MAX]; + printf(" Last Bad: %s\n", format_timestamp(buf, sizeof(buf), hr->last_bad_authentication_usec)); + } + + t = user_record_ratelimit_next_try(hr); + if (t != USEC_INFINITY) { + usec_t n = now(CLOCK_REALTIME); + + if (t <= n) + printf(" Next Try: anytime\n"); + else { + char buf[FORMAT_TIMESPAN_MAX]; + printf(" Next Try: %sin %s%s\n", + ansi_highlight_red(), + format_timespan(buf, sizeof(buf), t - n, USEC_PER_SEC), + ansi_normal()); + } + } + + if (storage != USER_CLASSIC) { + char buf[FORMAT_TIMESPAN_MAX]; + printf(" Auth. Limit: %" PRIu64 " attempts per %s\n", user_record_ratelimit_burst(hr), + format_timespan(buf, sizeof(buf), user_record_ratelimit_interval_usec(hr), 0)); + } + + if (hr->enforce_password_policy >= 0) + printf(" Passwd Pol.: %s\n", yes_no(hr->enforce_password_policy)); + + if (hr->password_change_min_usec != UINT64_MAX || + hr->password_change_max_usec != UINT64_MAX || + hr->password_change_warn_usec != UINT64_MAX || + hr->password_change_inactive_usec != UINT64_MAX) { + + char buf[FORMAT_TIMESPAN_MAX]; + printf(" Passwd Chg.:"); + + if (hr->password_change_min_usec != UINT64_MAX) { + printf(" min %s", format_timespan(buf, sizeof(buf), hr->password_change_min_usec, 0)); + + if (hr->password_change_max_usec != UINT64_MAX) + printf(" …"); + } + + if (hr->password_change_max_usec != UINT64_MAX) + printf(" max %s", format_timespan(buf, sizeof(buf), hr->password_change_max_usec, 0)); + + if (hr->password_change_warn_usec != UINT64_MAX) + printf("/warn %s", format_timespan(buf, sizeof(buf), hr->password_change_warn_usec, 0)); + + if (hr->password_change_inactive_usec != UINT64_MAX) + printf("/inactive %s", format_timespan(buf, sizeof(buf), hr->password_change_inactive_usec, 0)); + + printf("\n"); + } + + if (hr->password_change_now >= 0) + printf("Pas. Ch. Now: %s\n", yes_no(hr->password_change_now)); + + if (!strv_isempty(hr->ssh_authorized_keys)) + printf("SSH Pub. Key: %zu\n", strv_length(hr->ssh_authorized_keys)); + + if (!strv_isempty(hr->pkcs11_token_uri)) { + char **i; + + STRV_FOREACH(i, hr->pkcs11_token_uri) + printf(i == hr->pkcs11_token_uri ? + " Sec. Token: %s\n" : + " %s\n", *i); + } + + k = strv_length(hr->hashed_password); + if (k == 0) + printf(" Passwords: %snone%s\n", + user_record_disposition(hr) == USER_REGULAR ? ansi_highlight_yellow() : ansi_normal(), ansi_normal()); + else + printf(" Passwords: %zu\n", k); + + if (hr->signed_locally >= 0) + printf(" Local Sig.: %s\n", yes_no(hr->signed_locally)); + + if (hr->stop_delay_usec != UINT64_MAX) { + char buf[FORMAT_TIMESPAN_MAX]; + printf(" Stop Delay: %s\n", format_timespan(buf, sizeof(buf), hr->stop_delay_usec, 0)); + } + + if (hr->auto_login >= 0) + printf("Autom. Login: %s\n", yes_no(hr->auto_login)); + + if (hr->kill_processes >= 0) + printf(" Kill Proc.: %s\n", yes_no(hr->kill_processes)); + + if (hr->service) + printf(" Service: %s\n", hr->service); +} diff --git a/src/shared/user-record-show.h b/src/shared/user-record-show.h new file mode 100644 index 0000000000..bd22be2ae0 --- /dev/null +++ b/src/shared/user-record-show.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "user-record.h" + +const char *user_record_state_color(const char *state); + +void user_record_show(UserRecord *hr, bool show_full_group_info); From d093b62c941ebd6cf16ef9dd6430841eb361ed92 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 18:33:30 +0200 Subject: [PATCH 08/29] userdbd: add new service that can merge userdb queries from multiple clients --- meson.build | 24 + meson_options.txt | 2 + src/userdb/meson.build | 11 + src/userdb/userdbd-manager.c | 302 ++++++++++++ src/userdb/userdbd-manager.h | 34 ++ src/userdb/userdbd.c | 56 +++ src/userdb/userwork.c | 778 +++++++++++++++++++++++++++++++ units/meson.build | 3 + units/systemd-userdbd.service.in | 41 ++ units/systemd-userdbd.socket | 19 + 10 files changed, 1270 insertions(+) create mode 100644 src/userdb/meson.build create mode 100644 src/userdb/userdbd-manager.c create mode 100644 src/userdb/userdbd-manager.h create mode 100644 src/userdb/userdbd.c create mode 100644 src/userdb/userwork.c create mode 100644 units/systemd-userdbd.service.in create mode 100644 units/systemd-userdbd.socket diff --git a/meson.build b/meson.build index 3b3786bbdb..16ef6ce835 100644 --- a/meson.build +++ b/meson.build @@ -243,6 +243,7 @@ conf.set_quoted('SYSTEMD_EXPORT_PATH', join_paths(rootlib conf.set_quoted('VENDOR_KEYRING_PATH', join_paths(rootlibexecdir, 'import-pubring.gpg')) conf.set_quoted('USER_KEYRING_PATH', join_paths(pkgsysconfdir, 'import-pubring.gpg')) conf.set_quoted('DOCUMENT_ROOT', join_paths(pkgdatadir, 'gatewayd')) +conf.set_quoted('SYSTEMD_USERWORK_PATH', join_paths(rootlibexecdir, 'systemd-userwork')) conf.set10('MEMORY_ACCOUNTING_DEFAULT', memory_accounting_default) conf.set_quoted('MEMORY_ACCOUNTING_DEFAULT_YES_NO', memory_accounting_default ? 'yes' : 'no') conf.set('STATUS_UNIT_FORMAT_DEFAULT', 'STATUS_UNIT_FORMAT_' + status_unit_format_default.to_upper()) @@ -1322,6 +1323,7 @@ foreach term : ['utmp', 'localed', 'machined', 'portabled', + 'userdb', 'networkd', 'timedated', 'timesyncd', @@ -1538,6 +1540,7 @@ subdir('src/kernel-install') subdir('src/locale') subdir('src/machine') subdir('src/portable') +subdir('src/userdb') subdir('src/nspawn') subdir('src/resolve') subdir('src/timedate') @@ -1974,6 +1977,26 @@ 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) +endif + foreach alias : ['halt', 'poweroff', 'reboot', 'runlevel', 'shutdown', 'telinit'] meson.add_install_script(meson_make_symlink, join_paths(rootbindir, 'systemctl'), @@ -3252,6 +3275,7 @@ foreach tuple : [ ['logind'], ['machined'], ['portabled'], + ['userdb'], ['importd'], ['hostnamed'], ['timedated'], diff --git a/meson_options.txt b/meson_options.txt index 4d641c3671..6736240f39 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -94,6 +94,8 @@ option('machined', type : 'boolean', description : 'install the systemd-machined stack') option('portabled', type : 'boolean', description : 'install the systemd-portabled stack') +option('userdb', type : 'boolean', + description : 'install the systemd-userdbd stack') option('networkd', type : 'boolean', description : 'install the systemd-networkd stack') option('timedated', type : 'boolean', diff --git a/src/userdb/meson.build b/src/userdb/meson.build new file mode 100644 index 0000000000..72d2f7da08 --- /dev/null +++ b/src/userdb/meson.build @@ -0,0 +1,11 @@ +# 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()) diff --git a/src/userdb/userdbd-manager.c b/src/userdb/userdbd-manager.c new file mode 100644 index 0000000000..2836ca0409 --- /dev/null +++ b/src/userdb/userdbd-manager.c @@ -0,0 +1,302 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "sd-daemon.h" + +#include "fd-util.h" +#include "fs-util.h" +#include "mkdir.h" +#include "process-util.h" +#include "set.h" +#include "signal-util.h" +#include "socket-util.h" +#include "stdio-util.h" +#include "umask-util.h" +#include "userdbd-manager.h" + +#define LISTEN_TIMEOUT_USEC (25 * USEC_PER_SEC) + +static int start_workers(Manager *m, bool explicit_request); + +static int on_sigchld(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *m = userdata; + + assert(s); + assert(m); + + for (;;) { + siginfo_t siginfo = {}; + bool removed = false; + + if (waitid(P_ALL, 0, &siginfo, WNOHANG|WEXITED) < 0) { + if (errno == ECHILD) + break; + + log_warning_errno(errno, "Failed to invoke waitid(): %m"); + break; + } + if (siginfo.si_pid == 0) + break; + + if (set_remove(m->workers_dynamic, PID_TO_PTR(siginfo.si_pid))) + removed = true; + if (set_remove(m->workers_fixed, PID_TO_PTR(siginfo.si_pid))) + removed = true; + + if (!removed) { + log_warning("Weird, got SIGCHLD for unknown child " PID_FMT ", ignoring.", siginfo.si_pid); + continue; + } + + if (siginfo.si_code == CLD_EXITED) { + if (siginfo.si_status == EXIT_SUCCESS) + log_debug("Worker " PID_FMT " exited successfully.", siginfo.si_pid); + else + log_warning("Worker " PID_FMT " died with a failure exit status %i, ignoring.", siginfo.si_pid, siginfo.si_status); + } else if (siginfo.si_code == CLD_KILLED) + log_warning("Worker " PID_FMT " was killed by signal %s, ignoring.", siginfo.si_pid, signal_to_string(siginfo.si_status)); + else if (siginfo.si_code == CLD_DUMPED) + log_warning("Worker " PID_FMT " dumped core by signal %s, ignoring.", siginfo.si_pid, signal_to_string(siginfo.si_status)); + else + log_warning("Can't handle SIGCHLD of this type"); + } + + (void) start_workers(m, false); /* Fill up workers again if we fell below the low watermark */ + return 0; +} + +static int on_sigusr2(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *m = userdata; + + assert(s); + assert(m); + + (void) start_workers(m, true); /* Workers told us there's more work, let's add one more worker as long as we are below the high watermark */ + return 0; +} + +int manager_new(Manager **ret) { + Manager *m; + int r; + + m = new(Manager, 1); + if (!m) + return -ENOMEM; + + *m = (Manager) { + .listen_fd = -1, + .worker_ratelimit = { + .interval = 5 * USEC_PER_SEC, + .burst = 50, + }, + }; + + r = sd_event_new(&m->event); + if (r < 0) + return r; + + r = sd_event_add_signal(m->event, NULL, SIGINT, NULL, NULL); + if (r < 0) + return r; + + r = sd_event_add_signal(m->event, NULL, SIGTERM, NULL, NULL); + if (r < 0) + return r; + + (void) sd_event_set_watchdog(m->event, true); + + m->workers_fixed = set_new(NULL); + m->workers_dynamic = set_new(NULL); + + if (!m->workers_fixed || !m->workers_dynamic) + return -ENOMEM; + + r = sd_event_add_signal(m->event, &m->sigusr2_event_source, SIGUSR2, on_sigusr2, m); + if (r < 0) + return r; + + r = sd_event_add_signal(m->event, &m->sigchld_event_source, SIGCHLD, on_sigchld, m); + if (r < 0) + return r; + + *ret = TAKE_PTR(m); + return 0; +} + +Manager* manager_free(Manager *m) { + if (!m) + return NULL; + + set_free(m->workers_fixed); + set_free(m->workers_dynamic); + + sd_event_source_disable_unref(m->sigusr2_event_source); + sd_event_source_disable_unref(m->sigchld_event_source); + + sd_event_unref(m->event); + + return mfree(m); +} + +static size_t manager_current_workers(Manager *m) { + assert(m); + + return set_size(m->workers_fixed) + set_size(m->workers_dynamic); +} + +static int start_one_worker(Manager *m) { + bool fixed; + pid_t pid; + int r; + + assert(m); + + fixed = set_size(m->workers_fixed) < USERDB_WORKERS_MIN; + + r = safe_fork("(sd-worker)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_LOG, &pid); + if (r < 0) + return log_error_errno(r, "Failed to fork new worker child: %m"); + if (r == 0) { + char pids[DECIMAL_STR_MAX(pid_t)]; + /* Child */ + + log_close(); + + r = close_all_fds(&m->listen_fd, 1); + if (r < 0) { + log_error_errno(r, "Failed to close fds in child: %m"); + _exit(EXIT_FAILURE); + } + + log_open(); + + if (m->listen_fd == 3) { + r = fd_cloexec(3, false); + if (r < 0) { + log_error_errno(r, "Failed to turn off O_CLOEXEC for fd 3: %m"); + _exit(EXIT_FAILURE); + } + } else { + if (dup2(m->listen_fd, 3) < 0) { /* dup2() creates with O_CLOEXEC off */ + log_error_errno(errno, "Failed to move listen fd to 3: %m"); + _exit(EXIT_FAILURE); + } + + safe_close(m->listen_fd); + } + + xsprintf(pids, PID_FMT, pid); + if (setenv("LISTEN_PID", pids, 1) < 0) { + log_error_errno(errno, "Failed to set $LISTEN_PID: %m"); + _exit(EXIT_FAILURE); + } + + if (setenv("LISTEN_FDS", "1", 1) < 0) { + log_error_errno(errno, "Failed to set $LISTEN_FDS: %m"); + _exit(EXIT_FAILURE); + } + + + if (setenv("USERDB_FIXED_WORKER", one_zero(fixed), 1) < 0) { + log_error_errno(errno, "Failed to set $USERDB_FIXED_WORKER: %m"); + _exit(EXIT_FAILURE); + } + + /* execl("/home/lennart/projects/systemd/build/systemd-userwork", "systemd-userwork", "xxxxxxxxxxxxxxxx", NULL); /\* With some extra space rename_process() can make use of *\/ */ + /* execl("/usr/bin/valgrind", "valgrind", "/home/lennart/projects/systemd/build/systemd-userwork", "systemd-userwork", "xxxxxxxxxxxxxxxx", NULL); /\* With some extra space rename_process() can make use of *\/ */ + + execl(SYSTEMD_USERWORK_PATH, "systemd-userwork", "xxxxxxxxxxxxxxxx", NULL); /* With some extra space rename_process() can make use of */ + log_error_errno(errno, "Failed start worker process: %m"); + _exit(EXIT_FAILURE); + } + + if (fixed) + r = set_put(m->workers_fixed, PID_TO_PTR(pid)); + else + r = set_put(m->workers_dynamic, PID_TO_PTR(pid)); + if (r < 0) + return log_error_errno(r, "Failed to add child process to set: %m"); + + return 0; +} + +static int start_workers(Manager *m, bool explicit_request) { + int r; + + assert(m); + + for (;;) { + size_t n; + + n = manager_current_workers(m); + if (n >= USERDB_WORKERS_MIN && (!explicit_request || n >= USERDB_WORKERS_MAX)) + break; + + if (!ratelimit_below(&m->worker_ratelimit)) { + /* If we keep starting workers too often, let's fail the whole daemon, something is wrong */ + sd_event_exit(m->event, EXIT_FAILURE); + + return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), "Worker threads requested too frequently, something is wrong."); + } + + r = start_one_worker(m); + if (r < 0) + return r; + + explicit_request = false; + } + + return 0; +} + +int manager_startup(Manager *m) { + struct timeval ts; + int n, r; + + assert(m); + assert(m->listen_fd < 0); + + n = sd_listen_fds(false); + if (n < 0) + return log_error_errno(n, "Failed to determine number of passed file descriptors: %m"); + if (n > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected one listening fd, got %i.", n); + if (n == 1) + m->listen_fd = SD_LISTEN_FDS_START; + else { + union sockaddr_union sockaddr; + + r = sockaddr_un_set_path(&sockaddr.un, "/run/systemd/userdb/io.systemd.NameServiceSwitch"); + if (r < 0) + return log_error_errno(r, "Cannot assign socket path to socket address: %m"); + + r = mkdir_p("/run/systemd/userdb", 0755); + if (r < 0) + return log_error_errno(r, "Failed to create /run/systemd/userdb: %m"); + + m->listen_fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0); + if (m->listen_fd < 0) + return log_error_errno(errno, "Failed to bind on socket: %m"); + + (void) sockaddr_un_unlink(&sockaddr.un); + + RUN_WITH_UMASK(0000) + if (bind(m->listen_fd, &sockaddr.sa, SOCKADDR_UN_LEN(sockaddr.un)) < 0) + return log_error_errno(errno, "Failed to bind socket: %m"); + + r = symlink_idempotent("io.systemd.NameServiceSwitch", "/run/systemd/userdb/io.systemd.Multiplexer", false); + if (r < 0) + return log_error_errno(r, "Failed to bind io.systemd.Multiplexer: %m"); + + if (listen(m->listen_fd, SOMAXCONN) < 0) + return log_error_errno(errno, "Failed to listen on socket: %m"); + } + + /* Let's make sure every accept() call on this socket times out after 25s. This allows workers to be + * GC'ed on idle */ + if (setsockopt(m->listen_fd, SOL_SOCKET, SO_RCVTIMEO, timeval_store(&ts, LISTEN_TIMEOUT_USEC), sizeof(ts)) < 0) + return log_error_errno(errno, "Failed to se SO_RCVTIMEO: %m"); + + return start_workers(m, false); +} diff --git a/src/userdb/userdbd-manager.h b/src/userdb/userdbd-manager.h new file mode 100644 index 0000000000..0bf67fe55b --- /dev/null +++ b/src/userdb/userdbd-manager.h @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "sd-bus.h" +#include "sd-event.h" + +typedef struct Manager Manager; + +#include "hashmap.h" +#include "varlink.h" +#include "ratelimit.h" + +#define USERDB_WORKERS_MIN 3 +#define USERDB_WORKERS_MAX 4096 + +struct Manager { + sd_event *event; + + Set *workers_fixed; /* Workers 0…USERDB_WORKERS_MIN */ + Set *workers_dynamic; /* Workers USERD_WORKERS_MIN+1…USERDB_WORKERS_MAX */ + + sd_event_source *sigusr2_event_source; + sd_event_source *sigchld_event_source; + + int listen_fd; + + RateLimit worker_ratelimit; +}; + +int manager_new(Manager **ret); +Manager* manager_free(Manager *m); +DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free); + +int manager_startup(Manager *m); diff --git a/src/userdb/userdbd.c b/src/userdb/userdbd.c new file mode 100644 index 0000000000..978fd1d788 --- /dev/null +++ b/src/userdb/userdbd.c @@ -0,0 +1,56 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include + +#include "daemon-util.h" +#include "userdbd-manager.h" +#include "log.h" +#include "main-func.h" +#include "signal-util.h" + +/* This service offers two Varlink services, both implementing io.systemd.UserDatabase: + * + * → io.systemd.NameServiceSwitch: this is a compatibility interface for glibc NSS: it response to + * name lookups by checking the classic NSS interfaces and responding that. + * + * → io.systemd.Multiplexer: this multiplexes lookup requests to all Varlink services that have a + * socket in /run/systemd/userdb/. It's supposed to simplify clients that don't want to implement + * the full iterative logic on their own. + */ + +static int run(int argc, char *argv[]) { + _cleanup_(notify_on_cleanup) const char *notify_stop = NULL; + _cleanup_(manager_freep) Manager *m = NULL; + int r; + + log_setup_service(); + + umask(0022); + + if (argc != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments."); + + if (setenv("SYSTEMD_BYPASS_USERDB", "io.systemd.NameServiceSwitch:io.systemd.Multiplexer", 1) < 0) + return log_error_errno(errno, "Failed to se $SYSTEMD_BYPASS_USERDB: %m"); + + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, SIGTERM, SIGINT, SIGUSR2, -1) >= 0); + + r = manager_new(&m); + if (r < 0) + return log_error_errno(r, "Could not create manager: %m"); + + r = manager_startup(m); + if (r < 0) + return log_error_errno(r, "Failed to start up daemon: %m"); + + notify_stop = notify_start(NOTIFY_READY, NOTIFY_STOPPING); + + r = sd_event_loop(m->event); + if (r < 0) + return log_error_errno(r, "Event loop failed: %m"); + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/userdb/userwork.c b/src/userdb/userwork.c new file mode 100644 index 0000000000..df11b5b984 --- /dev/null +++ b/src/userdb/userwork.c @@ -0,0 +1,778 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include + +#include "sd-daemon.h" + +#include "env-util.h" +#include "fd-util.h" +#include "group-record-nss.h" +#include "group-record.h" +#include "main-func.h" +#include "process-util.h" +#include "strv.h" +#include "time-util.h" +#include "user-record-nss.h" +#include "user-record.h" +#include "user-util.h" +#include "userdb.h" +#include "varlink.h" + +#define ITERATIONS_MAX 64U +#define RUNTIME_MAX_USEC (5 * USEC_PER_MINUTE) +#define PRESSURE_SLEEP_TIME_USEC (50 * USEC_PER_MSEC) +#define CONNECTION_IDLE_USEC (15 * USEC_PER_SEC) +#define LISTEN_IDLE_USEC (90 * USEC_PER_SEC) + +typedef struct LookupParameters { + const char *user_name; + const char *group_name; + union { + uid_t uid; + gid_t gid; + }; + const char *service; +} LookupParameters; + +static int add_nss_service(JsonVariant **v) { + _cleanup_(json_variant_unrefp) JsonVariant *status = NULL, *z = NULL; + char buf[SD_ID128_STRING_MAX]; + sd_id128_t mid; + int r; + + assert(v); + + /* Patch in service field if it's missing. The assumption here is that this field is unset only for + * NSS records */ + + if (json_variant_by_key(*v, "service")) + return 0; + + r = sd_id128_get_machine(&mid); + if (r < 0) + return r; + + status = json_variant_ref(json_variant_by_key(*v, "status")); + z = json_variant_ref(json_variant_by_key(status, sd_id128_to_string(mid, buf))); + + if (json_variant_by_key(z, "service")) + return 0; + + r = json_variant_set_field_string(&z, "service", "io.systemd.NameServiceSwitch"); + if (r < 0) + return r; + + r = json_variant_set_field(&status, buf, z); + if (r < 0) + return r; + + return json_variant_set_field(v, "status", status); +} + +static int build_user_json(Varlink *link, UserRecord *ur, JsonVariant **ret) { + _cleanup_(user_record_unrefp) UserRecord *stripped = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + UserRecordLoadFlags flags; + uid_t peer_uid; + bool trusted; + int r; + + assert(ur); + assert(ret); + + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) { + log_debug_errno(r, "Unable to query peer UID, ignoring: %m"); + trusted = false; + } else + trusted = peer_uid == 0 || peer_uid == ur->uid; + + flags = USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_BINDING|USER_RECORD_STRIP_SECRET|USER_RECORD_ALLOW_STATUS|USER_RECORD_ALLOW_SIGNATURE; + if (trusted) + flags |= USER_RECORD_ALLOW_PRIVILEGED; + else + flags |= USER_RECORD_STRIP_PRIVILEGED; + + r = user_record_clone(ur, flags, &stripped); + if (r < 0) + return r; + + stripped->incomplete = + ur->incomplete || + (FLAGS_SET(ur->mask, USER_RECORD_PRIVILEGED) && + !FLAGS_SET(stripped->mask, USER_RECORD_PRIVILEGED)); + + v = json_variant_ref(stripped->json); + r = add_nss_service(&v); + if (r < 0) + return r; + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(v)), + JSON_BUILD_PAIR("incomplete", JSON_BUILD_BOOLEAN(stripped->incomplete)))); +} + +static int vl_method_get_user_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, uid), 0 }, + { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), 0 }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + LookupParameters p = { + .uid = UID_INVALID, + }; + int r; + + assert(parameters); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (streq_ptr(p.service, "io.systemd.NameServiceSwitch")) { + if (uid_is_valid(p.uid)) + r = nss_user_record_by_uid(p.uid, &hr); + else if (p.user_name) + r = nss_user_record_by_name(p.user_name, &hr); + else { + _cleanup_(json_variant_unrefp) JsonVariant *last = NULL; + + setpwent(); + + for (;;) { + _cleanup_(user_record_unrefp) UserRecord *z = NULL; + _cleanup_free_ char *sbuf = NULL; + struct passwd *pw; + struct spwd spwd; + + errno = 0; + pw = getpwent(); + if (!pw) { + if (errno != 0) + log_debug_errno(errno, "Failure while iterating through NSS user database, ignoring: %m"); + + break; + } + + r = nss_spwd_for_passwd(pw, &spwd, &sbuf); + if (r < 0) + log_debug_errno(r, "Failed to acquire shadow entry for user %s, ignoring: %m", pw->pw_name); + + r = nss_passwd_to_user_record(pw, NULL, &z); + if (r < 0) { + endpwent(); + return r; + } + + if (last) { + r = varlink_notify(link, last); + if (r < 0) { + endpwent(); + return r; + } + + last = json_variant_unref(last); + } + + r = build_user_json(link, z, &last); + if (r < 0) { + endpwent(); + return r; + } + } + + endpwent(); + + if (!last) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_reply(link, last); + } + + } else if (streq_ptr(p.service, "io.systemd.Multiplexer")) { + + if (uid_is_valid(p.uid)) + r = userdb_by_uid(p.uid, USERDB_AVOID_MULTIPLEXER, &hr); + else if (p.user_name) + r = userdb_by_name(p.user_name, USERDB_AVOID_MULTIPLEXER, &hr); + else { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *last = NULL; + + r = userdb_all(USERDB_AVOID_MULTIPLEXER, &iterator); + if (r < 0) + return r; + + for (;;) { + _cleanup_(user_record_unrefp) UserRecord *z = NULL; + + r = userdb_iterator_get(iterator, &z); + if (r == -ESRCH) + break; + if (r < 0) + return r; + + if (last) { + r = varlink_notify(link, last); + if (r < 0) + return r; + + last = json_variant_unref(last); + } + + r = build_user_json(link, z, &last); + if (r < 0) + return r; + } + + if (!last) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_reply(link, last); + } + } else + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + if (r == -ESRCH) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + if (r < 0) { + log_debug_errno(r, "User lookup failed abnormally: %m"); + return varlink_error(link, "io.systemd.UserDatabase.ServiceNotAvailable", NULL); + } + + if ((uid_is_valid(p.uid) && hr->uid != p.uid) || + (p.user_name && !streq(hr->user_name, p.user_name))) + return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + r = build_user_json(link, hr, &v); + if (r < 0) + return r; + + return varlink_reply(link, v); +} + +static int build_group_json(Varlink *link, GroupRecord *gr, JsonVariant **ret) { + _cleanup_(group_record_unrefp) GroupRecord *stripped = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + UserRecordLoadFlags flags; + uid_t peer_uid; + bool trusted; + int r; + + assert(gr); + assert(ret); + + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) { + log_debug_errno(r, "Unable to query peer UID, ignoring: %m"); + trusted = false; + } else + trusted = peer_uid == 0; + + flags = USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_BINDING|USER_RECORD_STRIP_SECRET|USER_RECORD_ALLOW_STATUS|USER_RECORD_ALLOW_SIGNATURE; + if (trusted) + flags |= USER_RECORD_ALLOW_PRIVILEGED; + else + flags |= USER_RECORD_STRIP_PRIVILEGED; + + r = group_record_clone(gr, flags, &stripped); + if (r < 0) + return r; + + stripped->incomplete = + gr->incomplete || + (FLAGS_SET(gr->mask, USER_RECORD_PRIVILEGED) && + !FLAGS_SET(stripped->mask, USER_RECORD_PRIVILEGED)); + + v = json_variant_ref(gr->json); + r = add_nss_service(&v); + if (r < 0) + return r; + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(v)), + JSON_BUILD_PAIR("incomplete", JSON_BUILD_BOOLEAN(stripped->incomplete)))); +} + +static int vl_method_get_group_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, gid), 0 }, + { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), 0 }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; + LookupParameters p = { + .gid = GID_INVALID, + }; + int r; + + assert(parameters); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (streq_ptr(p.service, "io.systemd.NameServiceSwitch")) { + + if (gid_is_valid(p.gid)) + r = nss_group_record_by_gid(p.gid, &g); + else if (p.group_name) + r = nss_group_record_by_name(p.group_name, &g); + else { + _cleanup_(json_variant_unrefp) JsonVariant *last = NULL; + + setgrent(); + + for (;;) { + _cleanup_(group_record_unrefp) GroupRecord *z = NULL; + _cleanup_free_ char *sbuf = NULL; + struct group *grp; + struct sgrp sgrp; + + errno = 0; + grp = getgrent(); + if (!grp) { + if (errno != 0) + log_debug_errno(errno, "Failure while iterating through NSS group database, ignoring: %m"); + + break; + } + + r = nss_sgrp_for_group(grp, &sgrp, &sbuf); + if (r < 0) + log_debug_errno(r, "Failed to acquire shadow entry for group %s, ignoring: %m", grp->gr_name); + + r = nss_group_to_group_record(grp, r >= 0 ? &sgrp : NULL, &z); + if (r < 0) { + endgrent(); + return r; + } + + if (last) { + r = varlink_notify(link, last); + if (r < 0) { + endgrent(); + return r; + } + + last = json_variant_unref(last); + } + + r = build_group_json(link, z, &last); + if (r < 0) { + endgrent(); + return r; + } + } + + endgrent(); + + if (!last) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_reply(link, last); + } + + } else if (streq_ptr(p.service, "io.systemd.Multiplexer")) { + + if (gid_is_valid(p.gid)) + r = groupdb_by_gid(p.gid, USERDB_AVOID_MULTIPLEXER, &g); + else if (p.group_name) + r = groupdb_by_name(p.group_name, USERDB_AVOID_MULTIPLEXER, &g); + else { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *last = NULL; + + r = groupdb_all(USERDB_AVOID_MULTIPLEXER, &iterator); + if (r < 0) + return r; + + for (;;) { + _cleanup_(group_record_unrefp) GroupRecord *z = NULL; + + r = groupdb_iterator_get(iterator, &z); + if (r == -ESRCH) + break; + if (r < 0) + return r; + + if (last) { + r = varlink_notify(link, last); + if (r < 0) + return r; + + last = json_variant_unref(last); + } + + r = build_group_json(link, z, &last); + if (r < 0) + return r; + } + + if (!last) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_reply(link, last); + } + } else + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + if (r == -ESRCH) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + if (r < 0) { + log_debug_errno(r, "Group lookup failed abnormally: %m"); + return varlink_error(link, "io.systemd.UserDatabase.ServiceNotAvailable", NULL); + } + + if ((uid_is_valid(p.gid) && g->gid != p.gid) || + (p.group_name && !streq(g->group_name, p.group_name))) + return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + r = build_group_json(link, g, &v); + if (r < 0) + return r; + + return varlink_reply(link, v); +} + +static int vl_method_get_memberships(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + static const JsonDispatch dispatch_table[] = { + { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), 0 }, + { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), 0 }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + LookupParameters p = {}; + int r; + + assert(parameters); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (streq_ptr(p.service, "io.systemd.NameServiceSwitch")) { + + if (p.group_name) { + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; + const char *last = NULL; + char **i; + + r = nss_group_record_by_name(p.group_name, &g); + if (r == -ESRCH) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + if (r < 0) + return r; + + STRV_FOREACH(i, g->members) { + + if (p.user_name && !streq_ptr(p.user_name, *i)) + continue; + + if (last) { + r = varlink_notifyb(link, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(g->group_name)))); + if (r < 0) + return r; + } + + last = *i; + } + + if (!last) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_replyb(link, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(g->group_name)))); + } else { + _cleanup_free_ char *last_user_name = NULL, *last_group_name = NULL; + + setgrent(); + + for (;;) { + struct group *grp; + const char* two[2], **users, **i; + + errno = 0; + grp = getgrent(); + if (!grp) { + if (errno != 0) + log_debug_errno(errno, "Failure while iterating through NSS group database, ignoring: %m"); + + break; + } + + if (p.user_name) { + if (!strv_contains(grp->gr_mem, p.user_name)) + continue; + + two[0] = p.user_name; + two[1] = NULL; + + users = two; + } else + users = (const char**) grp->gr_mem; + + STRV_FOREACH(i, users) { + + if (last_user_name) { + assert(last_group_name); + + r = varlink_notifyb(link, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name)))); + if (r < 0) { + endgrent(); + return r; + } + + free(last_user_name); + free(last_group_name); + } + + last_user_name = strdup(*i); + last_group_name = strdup(grp->gr_name); + if (!last_user_name || !last_group_name) { + endgrent(); + return -ENOMEM; + } + } + } + + endgrent(); + + if (!last_user_name) { + assert(!last_group_name); + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + } + + assert(last_group_name); + + return varlink_replyb(link, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name)))); + } + + } else if (streq_ptr(p.service, "io.systemd.Multiplexer")) { + + _cleanup_free_ char *last_user_name = NULL, *last_group_name = NULL; + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + + if (p.group_name) + r = membershipdb_by_group(p.group_name, USERDB_AVOID_MULTIPLEXER, &iterator); + else if (p.user_name) + r = membershipdb_by_user(p.user_name, USERDB_AVOID_MULTIPLEXER, &iterator); + else + r = membershipdb_all(USERDB_AVOID_MULTIPLEXER, &iterator); + if (r < 0) + return r; + + for (;;) { + _cleanup_free_ char *user_name = NULL, *group_name = NULL; + + r = membershipdb_iterator_get(iterator, &user_name, &group_name); + if (r == -ESRCH) + break; + if (r < 0) + return r; + + /* If both group + user are specified do a-posteriori filtering */ + if (p.group_name && p.user_name && !streq(group_name, p.group_name)) + continue; + + if (last_user_name) { + assert(last_group_name); + + r = varlink_notifyb(link, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name)))); + if (r < 0) + return r; + + free(last_user_name); + free(last_group_name); + } + + last_user_name = TAKE_PTR(user_name); + last_group_name = TAKE_PTR(group_name); + } + + if (!last_user_name) { + assert(!last_group_name); + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + } + + assert(last_group_name); + + return varlink_replyb(link, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)), + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name)))); + } + + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); +} + +static int process_connection(VarlinkServer *server, int fd) { + _cleanup_(varlink_close_unrefp) Varlink *vl = NULL; + int r; + + r = varlink_server_add_connection(server, fd, &vl); + if (r < 0) { + fd = safe_close(fd); + return log_error_errno(r, "Failed to add connection: %m"); + } + + vl = varlink_ref(vl); + + for (;;) { + r = varlink_process(vl); + if (r == -ENOTCONN) { + log_debug("Connection terminated."); + break; + } + if (r < 0) + return log_error_errno(r, "Failed to process connection: %m"); + if (r > 0) + continue; + + r = varlink_wait(vl, CONNECTION_IDLE_USEC); + if (r < 0) + return log_error_errno(r, "Failed to wait for connection events: %m"); + if (r == 0) + break; + } + + return 0; +} + +static int run(int argc, char *argv[]) { + usec_t start_time, listen_idle_usec, last_busy_usec = USEC_INFINITY; + _cleanup_(varlink_server_unrefp) VarlinkServer *server = NULL; + _cleanup_close_ int lock = -1; + unsigned n_iterations = 0; + int m, listen_fd, r; + + log_setup_service(); + + m = sd_listen_fds(false); + if (m < 0) + return log_error_errno(m, "Failed to determine number of listening fds: %m"); + if (m == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No socket to listen on received."); + if (m > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Worker can only listen on a single socket at a time."); + + listen_fd = SD_LISTEN_FDS_START; + + r = fd_nonblock(listen_fd, false); + if (r < 0) + return log_error_errno(r, "Failed to turn off non-blocking mode for listening socket: %m"); + + r = varlink_server_new(&server, 0); + if (r < 0) + return log_error_errno(r, "Failed to allocate server: %m"); + + r = varlink_server_bind_method_many( + server, + "io.systemd.UserDatabase.GetUserRecord", vl_method_get_user_record, + "io.systemd.UserDatabase.GetGroupRecord", vl_method_get_group_record, + "io.systemd.UserDatabase.GetMemberships", vl_method_get_memberships); + if (r < 0) + return log_error_errno(r, "Failed to bind methods: %m"); + + r = getenv_bool("USERDB_FIXED_WORKER"); + if (r < 0) + return log_error_errno(r, "Failed to parse USERDB_FIXED_WORKER: %m"); + listen_idle_usec = r ? USEC_INFINITY : LISTEN_IDLE_USEC; + + lock = userdb_nss_compat_disable(); + if (lock < 0) + return log_error_errno(r, "Failed to disable userdb NSS compatibility: %m"); + + start_time = now(CLOCK_MONOTONIC); + + for (;;) { + _cleanup_close_ int fd = -1; + usec_t n; + + /* Exit the worker in regular intervals, to flush out all memory use */ + if (n_iterations++ > ITERATIONS_MAX) { + log_debug("Exiting worker, processed %u iterations, that's enough.", n_iterations); + break; + } + + n = now(CLOCK_MONOTONIC); + if (n >= usec_add(start_time, RUNTIME_MAX_USEC)) { + char buf[FORMAT_TIMESPAN_MAX]; + log_debug("Exiting worker, ran for %s, that's enough.", format_timespan(buf, sizeof(buf), usec_sub_unsigned(n, start_time), 0)); + break; + } + + if (last_busy_usec == USEC_INFINITY) + last_busy_usec = n; + else if (listen_idle_usec != USEC_INFINITY && n >= usec_add(last_busy_usec, listen_idle_usec)) { + char buf[FORMAT_TIMESPAN_MAX]; + log_debug("Exiting worker, been idle for %s, .", format_timespan(buf, sizeof(buf), usec_sub_unsigned(n, last_busy_usec), 0)); + break; + } + + (void) rename_process("systemd-userwork: waiting..."); + + fd = accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK|SOCK_CLOEXEC); + if (fd < 0) + fd = -errno; + + (void) rename_process("systemd-userwork: processing..."); + + if (fd == -EAGAIN) + continue; /* The listening socket as SO_RECVTIMEO set, hence a time-out is expected + * after a while, let's check if it's time to exit though. */ + if (fd == -EINTR) + continue; /* Might be that somebody attached via strace, let's just continue in that + * case */ + if (fd < 0) + return log_error_errno(fd, "Failed to accept() from listening socket: %m"); + + if (now(CLOCK_MONOTONIC) <= usec_add(n, PRESSURE_SLEEP_TIME_USEC)) { + struct pollfd pfd = { + .fd = listen_fd, + .events = POLLIN, + }; + + /* We only slept a very short time? If so, let's see if there are more sockets + * pending, and if so, let's ask our parent for more workers */ + + if (poll(&pfd, 1, 0) < 0) + return log_error_errno(errno, "Failed to test for POLLIN on listening socket: %m"); + + if (FLAGS_SET(pfd.revents, POLLIN)) { + pid_t parent; + + parent = getppid(); + if (parent <= 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Parent already died?"); + + if (kill(parent, SIGUSR1) < 0) + return log_error_errno(errno, "Failed to kill our own parent."); + } + } + + (void) process_connection(server, TAKE_FD(fd)); + last_busy_usec = USEC_INFINITY; + } + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/units/meson.build b/units/meson.build index 4ad64d12f2..82c399bee8 100644 --- a/units/meson.build +++ b/units/meson.build @@ -96,6 +96,8 @@ units = [ 'sockets.target.wants/'], ['systemd-journald.socket', '', 'sockets.target.wants/'], + ['systemd-userdbd.socket', 'ENABLE_USERDB', + 'sockets.target.wants/'], ['systemd-networkd.socket', 'ENABLE_NETWORKD'], ['systemd-poweroff.service', ''], ['systemd-reboot.service', ''], @@ -182,6 +184,7 @@ in_units = [ ['systemd-nspawn@.service', ''], ['systemd-portabled.service', 'ENABLE_PORTABLED', 'dbus-org.freedesktop.portable1.service'], + ['systemd-userdbd.service', 'ENABLE_USERDB'], ['systemd-quotacheck.service', 'ENABLE_QUOTACHECK'], ['systemd-random-seed.service', 'ENABLE_RANDOMSEED', 'sysinit.target.wants/'], diff --git a/units/systemd-userdbd.service.in b/units/systemd-userdbd.service.in new file mode 100644 index 0000000000..e30ed2109e --- /dev/null +++ b/units/systemd-userdbd.service.in @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: LGPL-2.1+ +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=User Database Manager +Documentation=man:systemd-userdbd.service(8) +Requires=systemd-userdbd.socket +After=systemd-userdbd.socket +Before=sysinit.target +DefaultDependencies=no + +[Service] +CapabilityBoundingSet=CAP_DAC_READ_SEARCH +ExecStart=@rootlibexecdir@/systemd-userdbd +IPAddressDeny=any +LimitNOFILE=@HIGH_RLIMIT_NOFILE@ +LockPersonality=yes +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes +PrivateDevices=yes +ProtectControlGroups=yes +ProtectHome=yes +ProtectHostname=yes +ProtectKernelLogs=yes +ProtectKernelModules=yes +ProtectSystem=strict +RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6 +RestrictNamespaces=yes +RestrictRealtime=yes +RestrictSUIDSGID=yes +SystemCallArchitectures=native +SystemCallErrorNumber=EPERM +SystemCallFilter=@system-service +Type=notify +@SERVICE_WATCHDOG@ diff --git a/units/systemd-userdbd.socket b/units/systemd-userdbd.socket new file mode 100644 index 0000000000..1c749ea1d2 --- /dev/null +++ b/units/systemd-userdbd.socket @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: LGPL-2.1+ +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=User Database Manager Socket +Documentation=man:systemd-userdbd.service(8) +DefaultDependencies=no +Before=sockets.target + +[Socket] +ListenStream=/run/systemd/userdb/io.systemd.Multiplexer +Symlinks=/run/systemd/userdb/io.systemd.NameServiceSwitch +SocketMode=0666 From 1604937f83d3154fb1c3b5ef053f7fccd0825ce6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 5 Aug 2019 18:22:01 +0200 Subject: [PATCH 09/29] userdbd: add userdbctl tool as client for userdbd --- meson.build | 9 + src/userdb/meson.build | 4 + src/userdb/userdbctl.c | 790 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 803 insertions(+) create mode 100644 src/userdb/userdbctl.c diff --git a/meson.build b/meson.build index 16ef6ce835..6837fb9271 100644 --- a/meson.build +++ b/meson.build @@ -1995,6 +1995,15 @@ if conf.get('ENABLE_USERDB') == 1 install_rpath : rootlibexecdir, install : true, install_dir : rootlibexecdir) + + executable('userdbctl', + userdbctl_sources, + include_directories : includes, + link_with : [libshared], + dependencies : [threads], + install_rpath : rootlibexecdir, + install : true, + install_dir : rootbindir) endif foreach alias : ['halt', 'poweroff', 'reboot', 'runlevel', 'shutdown', 'telinit'] diff --git a/src/userdb/meson.build b/src/userdb/meson.build index 72d2f7da08..2f7e1accbf 100644 --- a/src/userdb/meson.build +++ b/src/userdb/meson.build @@ -9,3 +9,7 @@ systemd_userdbd_sources = files(''' userdbd-manager.h userdbd.c '''.split()) + +userdbctl_sources = files(''' + userdbctl.c +'''.split()) diff --git a/src/userdb/userdbctl.c b/src/userdb/userdbctl.c new file mode 100644 index 0000000000..9083797856 --- /dev/null +++ b/src/userdb/userdbctl.c @@ -0,0 +1,790 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include +#include + +#include "dirent-util.h" +#include "errno-list.h" +#include "fd-util.h" +#include "format-table.h" +#include "format-util.h" +#include "group-record-show.h" +#include "main-func.h" +#include "pager.h" +#include "parse-util.h" +#include "pretty-print.h" +#include "socket-util.h" +#include "strv.h" +#include "terminal-util.h" +#include "user-record-show.h" +#include "user-util.h" +#include "userdb.h" +#include "verbs.h" + +static enum { + OUTPUT_CLASSIC, + OUTPUT_TABLE, + OUTPUT_FRIENDLY, + OUTPUT_JSON, + _OUTPUT_INVALID = -1 +} arg_output = _OUTPUT_INVALID; + +static PagerFlags arg_pager_flags = 0; +static bool arg_legend = true; +static char** arg_services = NULL; +static UserDBFlags arg_userdb_flags = 0; + +STATIC_DESTRUCTOR_REGISTER(arg_services, strv_freep); + +static int show_user(UserRecord *ur, Table *table) { + int r; + + assert(ur); + + switch (arg_output) { + + case OUTPUT_CLASSIC: + if (!uid_is_valid(ur->uid)) + break; + + printf("%s:x:" UID_FMT ":" GID_FMT ":%s:%s:%s\n", + ur->user_name, + ur->uid, + user_record_gid(ur), + strempty(user_record_real_name(ur)), + user_record_home_directory(ur), + user_record_shell(ur)); + + break; + + case OUTPUT_JSON: + json_variant_dump(ur->json, JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_PRETTY, NULL, 0); + break; + + case OUTPUT_FRIENDLY: + user_record_show(ur, true); + + if (ur->incomplete) { + fflush(stdout); + log_warning("Warning: lacking rights to acquire privileged fields of user record of '%s', output incomplete.", ur->user_name); + } + + break; + + case OUTPUT_TABLE: + assert(table); + + r = table_add_many( + table, + TABLE_STRING, ur->user_name, + TABLE_STRING, user_disposition_to_string(user_record_disposition(ur)), + TABLE_UID, ur->uid, + TABLE_GID, user_record_gid(ur), + TABLE_STRING, empty_to_null(ur->real_name), + TABLE_STRING, user_record_home_directory(ur), + TABLE_STRING, user_record_shell(ur), + TABLE_INT, (int) user_record_disposition(ur)); + if (r < 0) + return log_error_errno(r, "Failed to add row to table: %m"); + + break; + + default: + assert_not_reached("Unexpected output mode"); + } + + return 0; +} + +static int display_user(int argc, char *argv[], void *userdata) { + _cleanup_(table_unrefp) Table *table = NULL; + bool draw_separator = false; + int ret = 0, r; + + if (arg_output < 0) + arg_output = argc > 1 ? OUTPUT_FRIENDLY : OUTPUT_TABLE; + + if (arg_output == OUTPUT_TABLE) { + table = table_new("name", "disposition", "uid", "gid", "realname", "home", "shell", "disposition-numeric"); + if (!table) + return log_oom(); + + (void) table_set_align_percent(table, table_get_cell(table, 0, 2), 100); + (void) table_set_align_percent(table, table_get_cell(table, 0, 3), 100); + (void) table_set_empty_string(table, "-"); + (void) table_set_sort(table, 7, 2, (size_t) -1); + (void) table_set_display(table, 0, 1, 2, 3, 4, 5, 6, (size_t) -1); + } + + if (argc > 1) { + char **i; + + STRV_FOREACH(i, argv + 1) { + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + uid_t uid; + + if (parse_uid(*i, &uid) >= 0) + r = userdb_by_uid(uid, arg_userdb_flags, &ur); + else + r = userdb_by_name(*i, arg_userdb_flags, &ur); + if (r < 0) { + if (r == -ESRCH) + log_error_errno(r, "User %s does not exist.", *i); + else if (r == -EHOSTDOWN) + log_error_errno(r, "Selected user database service is not available for this request."); + else + log_error_errno(r, "Failed to find user %s: %m", *i); + + if (ret >= 0) + ret = r; + } else { + if (draw_separator && arg_output == OUTPUT_FRIENDLY) + putchar('\n'); + + r = show_user(ur, table); + if (r < 0) + return r; + + draw_separator = true; + } + } + } else { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + + r = userdb_all(arg_userdb_flags, &iterator); + if (r < 0) + return log_error_errno(r, "Failed to enumerate users: %m"); + + for (;;) { + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + + r = userdb_iterator_get(iterator, &ur); + if (r == -ESRCH) + break; + if (r == -EHOSTDOWN) + return log_error_errno(r, "Selected user database service is not available for this request."); + if (r < 0) + return log_error_errno(r, "Failed acquire next user: %m"); + + if (draw_separator && arg_output == OUTPUT_FRIENDLY) + putchar('\n'); + + r = show_user(ur, table); + if (r < 0) + return r; + + draw_separator = true; + } + } + + if (table) { + r = table_print(table, NULL); + if (r < 0) + return log_error_errno(r, "Failed to show table: %m"); + } + + return ret; +} + +static int show_group(GroupRecord *gr, Table *table) { + int r; + + assert(gr); + + switch (arg_output) { + + case OUTPUT_CLASSIC: { + _cleanup_free_ char *m = NULL; + + if (!gid_is_valid(gr->gid)) + break; + + m = strv_join(gr->members, ","); + if (!m) + return log_oom(); + + printf("%s:x:" GID_FMT ":%s\n", + gr->group_name, + gr->gid, + m); + break; + } + + case OUTPUT_JSON: + json_variant_dump(gr->json, JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_PRETTY, NULL, 0); + break; + + case OUTPUT_FRIENDLY: + group_record_show(gr, true); + + if (gr->incomplete) { + fflush(stdout); + log_warning("Warning: lacking rights to acquire privileged fields of group record of '%s', output incomplete.", gr->group_name); + } + + break; + + case OUTPUT_TABLE: + assert(table); + + r = table_add_many( + table, + TABLE_STRING, gr->group_name, + TABLE_STRING, user_disposition_to_string(group_record_disposition(gr)), + TABLE_GID, gr->gid, + TABLE_INT, (int) group_record_disposition(gr)); + if (r < 0) + return log_error_errno(r, "Failed to add row to table: %m"); + + break; + + default: + assert_not_reached("Unexpected disply mode"); + } + + return 0; +} + + +static int display_group(int argc, char *argv[], void *userdata) { + _cleanup_(table_unrefp) Table *table = NULL; + bool draw_separator = false; + int ret = 0, r; + + if (arg_output < 0) + arg_output = argc > 1 ? OUTPUT_FRIENDLY : OUTPUT_TABLE; + + if (arg_output == OUTPUT_TABLE) { + table = table_new("name", "disposition", "gid", "disposition-numeric"); + if (!table) + return log_oom(); + + (void) table_set_align_percent(table, table_get_cell(table, 0, 2), 100); + (void) table_set_sort(table, 3, 2, (size_t) -1); + (void) table_set_display(table, 0, 1, 2, (size_t) -1); + } + + if (argc > 1) { + char **i; + + STRV_FOREACH(i, argv + 1) { + _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; + gid_t gid; + + if (parse_gid(*i, &gid) >= 0) + r = groupdb_by_gid(gid, arg_userdb_flags, &gr); + else + r = groupdb_by_name(*i, arg_userdb_flags, &gr); + if (r < 0) { + if (r == -ESRCH) + log_error_errno(r, "Group %s does not exist.", *i); + else if (r == -EHOSTDOWN) + log_error_errno(r, "Selected group database service is not available for this request."); + else + log_error_errno(r, "Failed to find group %s: %m", *i); + + if (ret >= 0) + ret = r; + } else { + if (draw_separator && arg_output == OUTPUT_FRIENDLY) + putchar('\n'); + + r = show_group(gr, table); + if (r < 0) + return r; + + draw_separator = true; + } + } + + } else { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + + r = groupdb_all(arg_userdb_flags, &iterator); + if (r < 0) + return log_error_errno(r, "Failed to enumerate groups: %m"); + + for (;;) { + _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; + + r = groupdb_iterator_get(iterator, &gr); + if (r == -ESRCH) + break; + if (r == -EHOSTDOWN) + return log_error_errno(r, "Selected group database service is not available for this request."); + if (r < 0) + return log_error_errno(r, "Failed acquire next group: %m"); + + if (draw_separator && arg_output == OUTPUT_FRIENDLY) + putchar('\n'); + + r = show_group(gr, table); + if (r < 0) + return r; + + draw_separator = true; + } + + } + + if (table) { + r = table_print(table, NULL); + if (r < 0) + return log_error_errno(r, "Failed to show table: %m"); + } + + return ret; +} + +static int show_membership(const char *user, const char *group, Table *table) { + int r; + + assert(user); + assert(group); + + switch (arg_output) { + + case OUTPUT_CLASSIC: + /* Strictly speaking there's no 'classic' output for this concept, but let's output it in + * similar style to the classic output for user/group info */ + + printf("%s:%s\n", user, group); + break; + + case OUTPUT_JSON: { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + + r = json_build(&v, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("user", JSON_BUILD_STRING(user)), + JSON_BUILD_PAIR("group", JSON_BUILD_STRING(group)))); + if (r < 0) + return log_error_errno(r, "Failed to build JSON object: %m"); + + json_variant_dump(v, JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO, NULL, NULL); + break; + } + + case OUTPUT_FRIENDLY: + /* Hmm, this is not particularly friendly, but not sure how we could do this better */ + printf("%s: %s\n", group, user); + break; + + case OUTPUT_TABLE: + assert(table); + + r = table_add_many( + table, + TABLE_STRING, user, + TABLE_STRING, group); + if (r < 0) + return log_error_errno(r, "Failed to add row to table: %m"); + + break; + + default: + assert_not_reached("Unexpected output mode"); + } + + return 0; +} + +static int display_memberships(int argc, char *argv[], void *userdata) { + _cleanup_(table_unrefp) Table *table = NULL; + int ret = 0, r; + + if (arg_output < 0) + arg_output = OUTPUT_TABLE; + + if (arg_output == OUTPUT_TABLE) { + table = table_new("user", "group"); + if (!table) + return log_oom(); + + (void) table_set_sort(table, 0, 1, (size_t) -1); + } + + if (argc > 1) { + char **i; + + STRV_FOREACH(i, argv + 1) { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + + if (streq(argv[0], "users-in-group")) { + r = membershipdb_by_group(*i, arg_userdb_flags, &iterator); + if (r < 0) + return log_error_errno(r, "Failed to enumerate users in group: %m"); + } else if (streq(argv[0], "groups-of-user")) { + r = membershipdb_by_user(*i, arg_userdb_flags, &iterator); + if (r < 0) + return log_error_errno(r, "Failed to enumerate groups of user: %m"); + } else + assert_not_reached("Unexpected verb"); + + for (;;) { + _cleanup_free_ char *user = NULL, *group = NULL; + + r = membershipdb_iterator_get(iterator, &user, &group); + if (r == -ESRCH) + break; + if (r == -EHOSTDOWN) + return log_error_errno(r, "Selected membership database service is not available for this request."); + if (r < 0) + return log_error_errno(r, "Failed acquire next membership: %m"); + + r = show_membership(user, group, table); + if (r < 0) + return r; + } + } + } else { + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + + r = membershipdb_all(arg_userdb_flags, &iterator); + if (r < 0) + return log_error_errno(r, "Failed to enumerate memberships: %m"); + + for (;;) { + _cleanup_free_ char *user = NULL, *group = NULL; + + r = membershipdb_iterator_get(iterator, &user, &group); + if (r == -ESRCH) + break; + if (r == -EHOSTDOWN) + return log_error_errno(r, "Selected membership database service is not available for this request."); + if (r < 0) + return log_error_errno(r, "Failed acquire next membership: %m"); + + r = show_membership(user, group, table); + if (r < 0) + return r; + } + } + + if (table) { + r = table_print(table, NULL); + if (r < 0) + return log_error_errno(r, "Failed to show table: %m"); + } + + return ret; +} + +static int display_services(int argc, char *argv[], void *userdata) { + _cleanup_(table_unrefp) Table *t = NULL; + _cleanup_(closedirp) DIR *d = NULL; + struct dirent *de; + int r; + + d = opendir("/run/systemd/userdb/"); + if (!d) { + if (errno == ENOENT) { + log_info("No services."); + return 0; + } + + return log_error_errno(errno, "Failed to open /run/systemd/userdb/: %m"); + } + + t = table_new("service", "listening"); + if (!t) + return log_oom(); + + (void) table_set_sort(t, 0, (size_t) -1); + + FOREACH_DIRENT(de, d, return -errno) { + _cleanup_free_ char *j = NULL, *no = NULL; + union sockaddr_union sockaddr; + _cleanup_close_ int fd = -1; + + j = path_join("/run/systemd/userdb/", de->d_name); + if (!j) + return log_oom(); + + r = sockaddr_un_set_path(&sockaddr.un, j); + if (r < 0) + return log_error_errno(r, "Path %s does not fit in AF_UNIX socket address: %m", j); + + fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + if (fd < 0) + return log_error_errno(r, "Failed to allocate AF_UNIX/SOCK_STREAM socket: %m"); + + if (connect(fd, &sockaddr.un, SOCKADDR_UN_LEN(sockaddr.un)) < 0) { + no = strjoin("No (", errno_to_name(errno), ")"); + if (!no) + return log_oom(); + } + + r = table_add_many(t, + TABLE_STRING, de->d_name, + TABLE_STRING, no ?: "yes", + TABLE_SET_COLOR, no ? ansi_highlight_red() : ansi_highlight_green()); + if (r < 0) + return log_error_errno(r, "Failed to add table row: %m"); + } + + if (table_get_rows(t) <= 0) { + log_info("No services."); + return 0; + } + + if (arg_output == OUTPUT_JSON) + table_print_json(t, NULL, JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO); + else + table_print(t, NULL); + + return 0; +} + +static int ssh_authorized_keys(int argc, char *argv[], void *userdata) { + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + int r; + + if (!valid_user_group_name(argv[1])) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid user name '%s'.", argv[1]); + + r = userdb_by_name(argv[1], arg_userdb_flags, &ur); + if (r == -ESRCH) + log_error_errno(r, "User %s does not exist.", argv[1]); + else if (r == -EHOSTDOWN) + log_error_errno(r, "Selected user database service is not available for this request."); + else if (r < 0) + log_error_errno(r, "Failed to find user %s: %m", argv[1]); + + if (strv_isempty(ur->ssh_authorized_keys)) + log_debug("User record for %s has no public SSH keys.", argv[1]); + else { + char **i; + + STRV_FOREACH(i, ur->ssh_authorized_keys) + printf("%s\n", *i); + } + + if (ur->incomplete) { + fflush(stdout); + log_warning("Warning: lacking rights to acquire privileged fields of user record of '%s', output incomplete.", ur->user_name); + } + + return EXIT_SUCCESS; +} + +static int help(int argc, char *argv[], void *userdata) { + _cleanup_free_ char *link = NULL; + int r; + + (void) pager_open(arg_pager_flags); + + r = terminal_urlify_man("userdbctl", "1", &link); + if (r < 0) + return log_oom(); + + printf("%s [OPTIONS...] COMMAND ...\n\n" + "%sShow user and group information.%s\n" + "\nCommands:\n" + " user [USER…] Inspect user\n" + " group [GROUP…] Inspect group\n" + " users-in-group [GROUP…] Show users that are members of specified group(s)\n" + " groups-of-user [USER…] Show groups the specified user(s) is a member of\n" + " services Show enabled database services\n" + "\nOptions:\n" + " -h --help Show this help\n" + " --version Show package version\n" + " --no-pager Do not pipe output into a pager\n" + " --no-legend Do not show the headers and footers\n" + " --output=MODE Select output mode (classic, friendly, table, json)\n" + " -j Equivalent to --output=json\n" + " -s --service=SERVICE[:SERVICE…]\n" + " Query the specified service\n" + " --with-nss=BOOL Control whether to include glibc NSS data\n" + " -N Disable inclusion of glibc NSS data and disable synthesizing\n" + " (Same as --with-nss=no --synthesize=no)\n" + " --synthesize=BOOL Synthesize root/nobody user\n" + "\nSee the %s for details.\n" + , program_invocation_short_name + , ansi_highlight(), ansi_normal() + , link + ); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + + enum { + ARG_VERSION = 0x100, + ARG_NO_PAGER, + ARG_NO_LEGEND, + ARG_OUTPUT, + ARG_WITH_NSS, + ARG_SYNTHESIZE, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, + { "output", required_argument, NULL, ARG_OUTPUT }, + { "service", required_argument, NULL, 's' }, + { "with-nss", required_argument, NULL, ARG_WITH_NSS }, + { "synthesize", required_argument, NULL, ARG_SYNTHESIZE }, + {} + }; + + const char *e; + int r; + + assert(argc >= 0); + assert(argv); + + /* We are going to update this environment variable with our own, hence let's first read what is already set */ + e = getenv("SYSTEMD_ONLY_USERDB"); + if (e) { + char **l; + + l = strv_split(e, ":"); + if (!l) + return log_oom(); + + strv_free(arg_services); + arg_services = l; + } + + for (;;) { + int c; + + c = getopt_long(argc, argv, "hjs:N", options, NULL); + if (c < 0) + break; + + switch (c) { + + case 'h': + return help(0, NULL, NULL); + + case ARG_VERSION: + return version(); + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_NO_LEGEND: + arg_legend = false; + break; + + case ARG_OUTPUT: + if (streq(optarg, "classic")) + arg_output = OUTPUT_CLASSIC; + else if (streq(optarg, "friendly")) + arg_output = OUTPUT_FRIENDLY; + else if (streq(optarg, "json")) + arg_output = OUTPUT_JSON; + else if (streq(optarg, "table")) + arg_output = OUTPUT_TABLE; + else if (streq(optarg, "help")) { + puts("classic\n" + "friendly\n" + "json"); + return 0; + } else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid --output= mode: %s", optarg); + + break; + + case 'j': + arg_output = OUTPUT_JSON; + break; + + case 's': + if (isempty(optarg)) + arg_services = strv_free(arg_services); + else { + char **l; + + l = strv_split(optarg, ":"); + if (!l) + return log_oom(); + + r = strv_extend_strv(&arg_services, l, true); + if (r < 0) + return log_oom(); + } + + break; + + case 'N': + arg_userdb_flags |= USERDB_AVOID_NSS|USERDB_DONT_SYNTHESIZE; + break; + + case ARG_WITH_NSS: + r = parse_boolean(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse --with-nss= parameter: %s", optarg); + + SET_FLAG(arg_userdb_flags, USERDB_AVOID_NSS, !r); + break; + + case ARG_SYNTHESIZE: + r = parse_boolean(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse --synthesize= parameter: %s", optarg); + + SET_FLAG(arg_userdb_flags, USERDB_DONT_SYNTHESIZE, !r); + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached("Unhandled option"); + } + } + + return 1; +} + +static int run(int argc, char *argv[]) { + static const Verb verbs[] = { + { "help", VERB_ANY, VERB_ANY, 0, help }, + { "user", VERB_ANY, VERB_ANY, VERB_DEFAULT, display_user }, + { "group", VERB_ANY, VERB_ANY, 0, display_group }, + { "users-in-group", VERB_ANY, VERB_ANY, 0, display_memberships }, + { "groups-of-user", VERB_ANY, VERB_ANY, 0, display_memberships }, + { "services", VERB_ANY, 1, 0, display_services }, + + /* This one is a helper for sshd_config's AuthorizedKeysCommand= setting, it's not a + * user-facing verb and thus should not appear in man pages or --help texts. */ + { "ssh-authorized-keys", 2, 2, 0, ssh_authorized_keys }, + {} + }; + + int r; + + log_show_color(true); + log_parse_environment(); + log_open(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + if (arg_services) { + _cleanup_free_ char *e = NULL; + + e = strv_join(arg_services, ":"); + if (!e) + return log_oom(); + + if (setenv("SYSTEMD_ONLY_USERDB", e, true) < 0) + return log_error_errno(r, "Failed to set $SYSTEMD_ONLY_USERDB: %m"); + + log_info("Enabled services: %s", e); + } else { + if (unsetenv("SYSTEMD_ONLY_USERDB") < 0) + return log_error_errno(r, "Failed to unset $SYSTEMD_ONLY_USERDB: %m"); + } + + return dispatch_verb(argc, argv, verbs, NULL); +} + +DEFINE_MAIN_FUNCTION(run); From 4bad7eedae3db617bd2fa0d6eb664554b4236513 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Aug 2019 12:48:45 +0200 Subject: [PATCH 10/29] core: make return parameter of dynamic_user_lookup_name() optional --- src/core/dynamic-user.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/dynamic-user.c b/src/core/dynamic-user.c index 75373407b4..e00109ee4a 100644 --- a/src/core/dynamic-user.c +++ b/src/core/dynamic-user.c @@ -529,7 +529,6 @@ int dynamic_user_current(DynamicUser *d, uid_t *ret) { int r; assert(d); - assert(ret); /* Get the currently assigned UID for the user, if there's any. This simply pops the data from the storage socket, and pushes it back in right-away. */ @@ -545,7 +544,9 @@ int dynamic_user_current(DynamicUser *d, uid_t *ret) { if (r < 0) return r; - *ret = uid; + if (ret) + *ret = uid; + return 0; } @@ -732,7 +733,6 @@ int dynamic_user_lookup_name(Manager *m, const char *name, uid_t *ret) { assert(m); assert(name); - assert(ret); /* A friendly call for translating a dynamic user's name into its UID */ From 19d22d433d3a0d93c81be079ed23dfca004a905a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Aug 2019 14:58:59 +0200 Subject: [PATCH 11/29] core: add user/group resolution varlink interface to PID 1 --- src/core/core-varlink.c | 310 ++++++++++++++++++++++++++++++++++++++++ src/core/core-varlink.h | 7 + src/core/manager.c | 8 +- src/core/manager.h | 3 + src/core/meson.build | 2 + 5 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 src/core/core-varlink.c create mode 100644 src/core/core-varlink.h diff --git a/src/core/core-varlink.c b/src/core/core-varlink.c new file mode 100644 index 0000000000..628b9ccaaa --- /dev/null +++ b/src/core/core-varlink.c @@ -0,0 +1,310 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "core-varlink.h" +#include "mkdir.h" +#include "user-util.h" +#include "varlink.h" + +typedef struct LookupParameters { + const char *user_name; + const char *group_name; + union { + uid_t uid; + gid_t gid; + }; + const char *service; +} LookupParameters; + +static int build_user_json(const char *user_name, uid_t uid, JsonVariant **ret) { + assert(user_name); + assert(uid_is_valid(uid)); + assert(ret); + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("record", JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(user_name)), + JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(uid)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(uid)), + JSON_BUILD_PAIR("realName", JSON_BUILD_STRING("Dynamic User")), + JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_STRING("/")), + JSON_BUILD_PAIR("shell", JSON_BUILD_STRING(NOLOGIN)), + JSON_BUILD_PAIR("locked", JSON_BUILD_BOOLEAN(true)), + JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.DynamicUser")), + JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("dynamic")))))); +} + +static bool user_match_lookup_parameters(LookupParameters *p, const char *name, uid_t uid) { + assert(p); + + if (p->user_name && !streq(name, p->user_name)) + return false; + + if (uid_is_valid(p->uid) && uid != p->uid) + return false; + + return true; +} + +static int vl_method_get_user_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, uid), 0 }, + { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), JSON_SAFE }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + LookupParameters p = { + .uid = UID_INVALID, + }; + _cleanup_free_ char *found_name = NULL; + uid_t found_uid = UID_INVALID, uid; + Manager *m = userdata; + const char *un; + int r; + + assert(parameters); + assert(m); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (!streq_ptr(p.service, "io.systemd.DynamicUser")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + if (uid_is_valid(p.uid)) + r = dynamic_user_lookup_uid(m, p.uid, &found_name); + else if (p.user_name) + r = dynamic_user_lookup_name(m, p.user_name, &found_uid); + else { + Iterator i; + DynamicUser *d; + + HASHMAP_FOREACH(d, m->dynamic_users, i) { + r = dynamic_user_current(d, &uid); + if (r == -EAGAIN) /* not realized yet? */ + continue; + if (r < 0) + return r; + + if (!user_match_lookup_parameters(&p, d->name, uid)) + continue; + + if (v) { + r = varlink_notify(link, v); + if (r < 0) + return r; + + v = json_variant_unref(v); + } + + r = build_user_json(d->name, uid, &v); + if (r < 0) + return r; + } + + if (!v) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_reply(link, v); + } + if (r == -ESRCH) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + if (r < 0) + return r; + + uid = uid_is_valid(found_uid) ? found_uid : p.uid; + un = found_name ?: p.user_name; + + if (!user_match_lookup_parameters(&p, un, uid)) + return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + r = build_user_json(un, uid, &v); + if (r < 0) + return r; + + return varlink_reply(link, v); +} + +static int build_group_json(const char *group_name, gid_t gid, JsonVariant **ret) { + assert(group_name); + assert(gid_is_valid(gid)); + assert(ret); + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("record", JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(group_name)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(gid)), + JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.DynamicUser")), + JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("dynamic")))))); +} + +static bool group_match_lookup_parameters(LookupParameters *p, const char *name, gid_t gid) { + assert(p); + + if (p->group_name && !streq(name, p->group_name)) + return false; + + if (gid_is_valid(p->gid) && gid != p->gid) + return false; + + return true; +} + +static int vl_method_get_group_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, gid), 0 }, + { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), JSON_SAFE }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + LookupParameters p = { + .gid = GID_INVALID, + }; + _cleanup_free_ char *found_name = NULL; + uid_t found_gid = GID_INVALID, gid; + Manager *m = userdata; + const char *gn; + int r; + + assert(parameters); + assert(m); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (!streq_ptr(p.service, "io.systemd.DynamicUser")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + if (gid_is_valid(p.gid)) + r = dynamic_user_lookup_uid(m, (uid_t) p.gid, &found_name); + else if (p.group_name) + r = dynamic_user_lookup_name(m, p.group_name, (uid_t*) &found_gid); + else { + DynamicUser *d; + Iterator i; + + HASHMAP_FOREACH(d, m->dynamic_users, i) { + uid_t uid; + + r = dynamic_user_current(d, &uid); + if (r == -EAGAIN) + continue; + if (r < 0) + return r; + + if (!group_match_lookup_parameters(&p, d->name, (gid_t) uid)) + continue; + + if (v) { + r = varlink_notify(link, v); + if (r < 0) + return r; + + v = json_variant_unref(v); + } + + r = build_group_json(d->name, (gid_t) uid, &v); + if (r < 0) + return r; + } + + if (!v) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + + return varlink_reply(link, v); + } + if (r == -ESRCH) + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); + if (r < 0) + return r; + + gid = gid_is_valid(found_gid) ? found_gid : p.gid; + gn = found_name ?: p.group_name; + + if (!group_match_lookup_parameters(&p, gn, gid)) + return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + r = build_group_json(gn, gid, &v); + if (r < 0) + return r; + + return varlink_reply(link, v); +} + +static int vl_method_get_memberships(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), JSON_SAFE }, + { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), JSON_SAFE }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + LookupParameters p = {}; + int r; + + assert(parameters); + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (!streq_ptr(p.service, "io.systemd.DynamicUser")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + /* We don't support auxiliary groups with dynamic users. */ + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); +} + +int manager_varlink_init(Manager *m) { + _cleanup_(varlink_server_unrefp) VarlinkServer *s = NULL; + int r; + + assert(m); + + if (m->varlink_server) + return 0; + + if (!MANAGER_IS_SYSTEM(m)) + return 0; + + r = varlink_server_new(&s, VARLINK_SERVER_ACCOUNT_UID); + if (r < 0) + return log_error_errno(r, "Failed to allocate varlink server object: %m"); + + varlink_server_set_userdata(s, m); + + r = varlink_server_bind_method_many( + s, + "io.systemd.UserDatabase.GetUserRecord", vl_method_get_user_record, + "io.systemd.UserDatabase.GetGroupRecord", vl_method_get_group_record, + "io.systemd.UserDatabase.GetMemberships", vl_method_get_memberships); + if (r < 0) + return log_error_errno(r, "Failed to register varlink methods: %m"); + + (void) mkdir_p("/run/systemd/userdb", 0755); + + r = varlink_server_listen_address(s, "/run/systemd/userdb/io.systemd.DynamicUser", 0666); + if (r < 0) + return log_error_errno(r, "Failed to bind to varlink socket: %m"); + + r = varlink_server_attach_event(s, m->event, SD_EVENT_PRIORITY_NORMAL); + if (r < 0) + return log_error_errno(r, "Failed to attach varlink connection to event loop: %m"); + + m->varlink_server = TAKE_PTR(s); + return 0; +} + +void manager_varlink_done(Manager *m) { + assert(m); + + m->varlink_server = varlink_server_unref(m->varlink_server); +} diff --git a/src/core/core-varlink.h b/src/core/core-varlink.h new file mode 100644 index 0000000000..89818e2766 --- /dev/null +++ b/src/core/core-varlink.h @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "manager.h" + +int manager_varlink_init(Manager *m); +void manager_varlink_done(Manager *m); diff --git a/src/core/manager.c b/src/core/manager.c index dbd25af27e..cb0633e2ab 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -29,6 +29,7 @@ #include "bus-util.h" #include "clean-ipc.h" #include "clock-util.h" +#include "core-varlink.h" #include "dbus-job.h" #include "dbus-manager.h" #include "dbus-unit.h" @@ -44,8 +45,8 @@ #include "fileio.h" #include "fs-util.h" #include "hashmap.h" -#include "io-util.h" #include "install.h" +#include "io-util.h" #include "label.h" #include "locale-setup.h" #include "log.h" @@ -1346,6 +1347,7 @@ Manager* manager_free(Manager *m) { lookup_paths_flush_generator(&m->lookup_paths); bus_done(m); + manager_varlink_done(m); exec_runtime_vacuum(m); hashmap_free(m->exec_runtime_by_id); @@ -1703,6 +1705,10 @@ int manager_startup(Manager *m, FILE *serialization, FDSet *fds) { log_warning_errno(r, "Failed to deserialized tracked clients, ignoring: %m"); m->deserialized_subscribed = strv_free(m->deserialized_subscribed); + r = manager_varlink_init(m); + if (r < 0) + log_warning_errno(r, "Failed to set up Varlink server, ignoring: %m"); + /* Third, fire things up! */ manager_coldplug(m); diff --git a/src/core/manager.h b/src/core/manager.h index 8ca8e3885f..67f9af5fc6 100644 --- a/src/core/manager.h +++ b/src/core/manager.h @@ -16,6 +16,7 @@ #include "list.h" #include "prioq.h" #include "ratelimit.h" +#include "varlink.h" struct libmnt_monitor; typedef struct Unit Unit; @@ -422,6 +423,8 @@ struct Manager { unsigned notifygen; bool honor_device_enumeration; + + VarlinkServer *varlink_server; }; static inline usec_t manager_default_timeout_abort_usec(Manager *m) { diff --git a/src/core/meson.build b/src/core/meson.build index e0f08c057e..3586838f59 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -22,6 +22,8 @@ libcore_sources = ''' bpf-firewall.h cgroup.c cgroup.h + core-varlink.c + core-varlink.h dbus-automount.c dbus-automount.h dbus-cgroup.c From 1684c56f40f020e685e70b3d1785d596ff16f892 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 4 Jul 2019 18:31:11 +0200 Subject: [PATCH 12/29] nss: hook up nss-systemd with userdb varlink bits This changes nss-systemd to use the new varlink user/group APIs for looking up everything. (This also changes the factory /etc/nsswitch.conf line to use for hooking up nss-system to use glibc's [SUCCESS=merge] feature so that we can properly merge group membership lists). Fixes: #12492 --- factory/etc/nsswitch.conf | 2 +- meson.build | 9 +- src/nss-systemd/nss-systemd.c | 1012 +++++++++++++------------------ src/nss-systemd/nss-systemd.sym | 1 + src/nss-systemd/userdb-glue.c | 344 +++++++++++ src/nss-systemd/userdb-glue.h | 20 + 6 files changed, 784 insertions(+), 604 deletions(-) create mode 100644 src/nss-systemd/userdb-glue.c create mode 100644 src/nss-systemd/userdb-glue.h diff --git a/factory/etc/nsswitch.conf b/factory/etc/nsswitch.conf index 5470993e34..e7365cd142 100644 --- a/factory/etc/nsswitch.conf +++ b/factory/etc/nsswitch.conf @@ -1,7 +1,7 @@ # This file is part of systemd. passwd: compat mymachines systemd -group: compat mymachines systemd +group: compat [SUCCESS=merge] mymachines [SUCCESS=merge] systemd shadow: compat hosts: files mymachines resolve [!UNAVAIL=return] dns myhostname diff --git a/meson.build b/meson.build index 6837fb9271..46f3e9c7ba 100644 --- a/meson.build +++ b/meson.build @@ -1567,7 +1567,7 @@ test_dlopen = executable( build_by_default : want_tests != 'false') foreach tuple : [['myhostname', 'ENABLE_NSS_MYHOSTNAME'], - ['systemd', 'ENABLE_NSS_SYSTEMD'], + ['systemd', 'ENABLE_NSS_SYSTEMD', 'src/nss-systemd/userdb-glue.c src/nss-systemd/userdb-glue.h'], ['mymachines', 'ENABLE_NSS_MYMACHINES'], ['resolve', 'ENABLE_NSS_RESOLVE']] @@ -1578,9 +1578,14 @@ foreach tuple : [['myhostname', 'ENABLE_NSS_MYHOSTNAME'], sym = 'src/nss-@0@/nss-@0@.sym'.format(module) version_script_arg = join_paths(project_source_root, sym) + sources = ['src/nss-@0@/nss-@0@.c'.format(module)] + if tuple.length() > 2 + sources += tuple[2].split() + endif + nss = shared_library( 'nss_' + module, - 'src/nss-@0@/nss-@0@.c'.format(module), + sources, disable_mempool_c, version : '2', include_directories : includes, diff --git a/src/nss-systemd/nss-systemd.c b/src/nss-systemd/nss-systemd.c index 8ef1cd5ea9..34f886a8cc 100644 --- a/src/nss-systemd/nss-systemd.c +++ b/src/nss-systemd/nss-systemd.c @@ -3,28 +3,17 @@ #include #include -#include "sd-bus.h" - -#include "alloc-util.h" -#include "bus-common-errors.h" -#include "dirent-util.h" #include "env-util.h" +#include "errno-util.h" #include "fd-util.h" -#include "format-util.h" -#include "fs-util.h" -#include "list.h" +#include "group-record-nss.h" #include "macro.h" #include "nss-util.h" #include "signal-util.h" -#include "stdio-util.h" -#include "string-util.h" +#include "strv.h" #include "user-util.h" -#include "util.h" - -#define DYNAMIC_USER_GECOS "Dynamic User" -#define DYNAMIC_USER_PASSWD "*" /* locked */ -#define DYNAMIC_USER_DIR "/" -#define DYNAMIC_USER_SHELL NOLOGIN +#include "userdb-glue.h" +#include "userdb.h" static const struct passwd root_passwd = { .pw_name = (char*) "root", @@ -60,78 +49,32 @@ static const struct group nobody_group = { .gr_mem = (char*[]) { NULL }, }; -typedef struct UserEntry UserEntry; -typedef struct GetentData GetentData; - -struct UserEntry { - uid_t id; - char *name; - - GetentData *data; - LIST_FIELDS(UserEntry, entries); -}; - -struct GetentData { - /* As explained in NOTES section of getpwent_r(3) as 'getpwent_r() is not really - * reentrant since it shares the reading position in the stream with all other threads', - * we need to protect the data in UserEntry from multithreaded programs which may call - * setpwent(), getpwent_r(), or endpwent() simultaneously. So, each function locks the - * data by using the mutex below. */ +typedef struct GetentData { + /* As explained in NOTES section of getpwent_r(3) as 'getpwent_r() is not really reentrant since it + * shares the reading position in the stream with all other threads', we need to protect the data in + * UserDBIterator from multithreaded programs which may call setpwent(), getpwent_r(), or endpwent() + * simultaneously. So, each function locks the data by using the mutex below. */ pthread_mutex_t mutex; + UserDBIterator *iterator; - UserEntry *position; - LIST_HEAD(UserEntry, entries); + /* Applies to group iterations only: true while we iterate over groups defined through NSS, false + * otherwise. */ + bool by_membership; +} GetentData; + +static GetentData getpwent_data = { + .mutex = PTHREAD_MUTEX_INITIALIZER }; -static GetentData getpwent_data = { PTHREAD_MUTEX_INITIALIZER, NULL, NULL }; -static GetentData getgrent_data = { PTHREAD_MUTEX_INITIALIZER, NULL, NULL }; +static GetentData getgrent_data = { + .mutex = PTHREAD_MUTEX_INITIALIZER +}; NSS_GETPW_PROTOTYPES(systemd); NSS_GETGR_PROTOTYPES(systemd); -enum nss_status _nss_systemd_endpwent(void) _public_; -enum nss_status _nss_systemd_setpwent(int stayopen) _public_; -enum nss_status _nss_systemd_getpwent_r(struct passwd *result, char *buffer, size_t buflen, int *errnop) _public_; -enum nss_status _nss_systemd_endgrent(void) _public_; -enum nss_status _nss_systemd_setgrent(int stayopen) _public_; -enum nss_status _nss_systemd_getgrent_r(struct group *result, char *buffer, size_t buflen, int *errnop) _public_; - -static int direct_lookup_name(const char *name, uid_t *ret) { - _cleanup_free_ char *s = NULL; - const char *path; - int r; - - assert(name); - - /* Normally, we go via the bus to resolve names. That has the benefit that it is available from any mount - * namespace and subject to proper authentication. However, there's one problem: if our module is called from - * dbus-daemon itself we really can't use D-Bus to communicate. In this case, resort to a client-side hack, - * and look for the dynamic names directly. This is pretty ugly, but breaks the cyclic dependency. */ - - path = strjoina("/run/systemd/dynamic-uid/direct:", name); - r = readlink_malloc(path, &s); - if (r < 0) - return r; - - return parse_uid(s, ret); -} - -static int direct_lookup_uid(uid_t uid, char **ret) { - char path[STRLEN("/run/systemd/dynamic-uid/direct:") + DECIMAL_STR_MAX(uid_t) + 1], *s; - int r; - - xsprintf(path, "/run/systemd/dynamic-uid/direct:" UID_FMT, uid); - - r = readlink_malloc(path, &s); - if (r < 0) - return r; - if (!valid_user_group_name(s)) { /* extra safety check */ - free(s); - return -EINVAL; - } - - *ret = s; - return 0; -} +NSS_PWENT_PROTOTYPES(systemd); +NSS_GRENT_PROTOTYPES(systemd); +NSS_INITGROUPS_PROTOTYPE(systemd); enum nss_status _nss_systemd_getpwnam_r( const char *name, @@ -139,99 +82,49 @@ enum nss_status _nss_systemd_getpwnam_r( char *buffer, size_t buflen, int *errnop) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL; - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - uint32_t translated; - size_t l; - int bypass, r; + enum nss_status status; + int e; PROTECT_ERRNO; BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); assert(name); assert(pwd); + assert(errnop); - /* If the username is not valid, then we don't know it. Ideally libc would filter these for us anyway. We don't - * generate EINVAL here, because it isn't really out business to complain about invalid user names. */ + /* If the username is not valid, then we don't know it. Ideally libc would filter these for us + * anyway. We don't generate EINVAL here, because it isn't really out business to complain about + * invalid user names. */ if (!valid_user_group_name(name)) return NSS_STATUS_NOTFOUND; /* Synthesize entries for the root and nobody users, in case they are missing in /etc/passwd */ if (getenv_bool_secure("SYSTEMD_NSS_BYPASS_SYNTHETIC") <= 0) { + if (streq(name, root_passwd.pw_name)) { *pwd = root_passwd; return NSS_STATUS_SUCCESS; } - if (synthesize_nobody() && - streq(name, nobody_passwd.pw_name)) { + + if (streq(name, nobody_passwd.pw_name)) { + if (!synthesize_nobody()) + return NSS_STATUS_NOTFOUND; + *pwd = nobody_passwd; return NSS_STATUS_SUCCESS; } - } - /* Make sure that we don't go in circles when allocating a dynamic UID by checking our own database */ - if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0) + } else if (STR_IN_SET(name, root_passwd.pw_name, nobody_passwd.pw_name)) return NSS_STATUS_NOTFOUND; - bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS"); - if (bypass <= 0) { - r = sd_bus_open_system(&bus); - if (r < 0) - bypass = 1; - } - - if (bypass > 0) { - r = direct_lookup_name(name, (uid_t*) &translated); - if (r == -ENOENT) - return NSS_STATUS_NOTFOUND; - if (r < 0) - goto fail; - } else { - r = sd_bus_call_method(bus, - "org.freedesktop.systemd1", - "/org/freedesktop/systemd1", - "org.freedesktop.systemd1.Manager", - "LookupDynamicUserByName", - &error, - &reply, - "s", - name); - if (r < 0) { - if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_DYNAMIC_USER)) - return NSS_STATUS_NOTFOUND; - - goto fail; - } - - r = sd_bus_message_read(reply, "u", &translated); - if (r < 0) - goto fail; - } - - l = strlen(name); - if (buflen < l+1) { + status = userdb_getpwnam(name, pwd, buffer, buflen, &e); + if (IN_SET(status, NSS_STATUS_UNAVAIL, NSS_STATUS_TRYAGAIN)) { UNPROTECT_ERRNO; - *errnop = ERANGE; - return NSS_STATUS_TRYAGAIN; + *errnop = e; + return status; } - memcpy(buffer, name, l+1); - - pwd->pw_name = buffer; - pwd->pw_uid = (uid_t) translated; - pwd->pw_gid = (uid_t) translated; - pwd->pw_gecos = (char*) DYNAMIC_USER_GECOS; - pwd->pw_passwd = (char*) DYNAMIC_USER_PASSWD; - pwd->pw_dir = (char*) DYNAMIC_USER_DIR; - pwd->pw_shell = (char*) DYNAMIC_USER_SHELL; - - return NSS_STATUS_SUCCESS; - -fail: - UNPROTECT_ERRNO; - *errnop = -r; - return NSS_STATUS_UNAVAIL; + return status; } enum nss_status _nss_systemd_getpwuid_r( @@ -240,100 +133,45 @@ enum nss_status _nss_systemd_getpwuid_r( char *buffer, size_t buflen, int *errnop) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL; - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - _cleanup_free_ char *direct = NULL; - const char *translated; - size_t l; - int bypass, r; + enum nss_status status; + int e; PROTECT_ERRNO; BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); + assert(pwd); + assert(errnop); + if (!uid_is_valid(uid)) return NSS_STATUS_NOTFOUND; /* Synthesize data for the root user and for nobody in case they are missing from /etc/passwd */ if (getenv_bool_secure("SYSTEMD_NSS_BYPASS_SYNTHETIC") <= 0) { + if (uid == root_passwd.pw_uid) { *pwd = root_passwd; return NSS_STATUS_SUCCESS; } - if (synthesize_nobody() && - uid == nobody_passwd.pw_uid) { + + if (uid == nobody_passwd.pw_uid) { + if (!synthesize_nobody()) + return NSS_STATUS_NOTFOUND; + *pwd = nobody_passwd; return NSS_STATUS_SUCCESS; } - } - if (!uid_is_dynamic(uid)) + } else if (uid == root_passwd.pw_uid || uid == nobody_passwd.pw_uid) return NSS_STATUS_NOTFOUND; - if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0) - return NSS_STATUS_NOTFOUND; - - bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS"); - if (bypass <= 0) { - r = sd_bus_open_system(&bus); - if (r < 0) - bypass = 1; - } - - if (bypass > 0) { - r = direct_lookup_uid(uid, &direct); - if (r == -ENOENT) - return NSS_STATUS_NOTFOUND; - if (r < 0) - goto fail; - - translated = direct; - - } else { - r = sd_bus_call_method(bus, - "org.freedesktop.systemd1", - "/org/freedesktop/systemd1", - "org.freedesktop.systemd1.Manager", - "LookupDynamicUserByUID", - &error, - &reply, - "u", - (uint32_t) uid); - if (r < 0) { - if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_DYNAMIC_USER)) - return NSS_STATUS_NOTFOUND; - - goto fail; - } - - r = sd_bus_message_read(reply, "s", &translated); - if (r < 0) - goto fail; - } - - l = strlen(translated) + 1; - if (buflen < l) { + status = userdb_getpwuid(uid, pwd, buffer, buflen, &e); + if (IN_SET(status, NSS_STATUS_UNAVAIL, NSS_STATUS_TRYAGAIN)) { UNPROTECT_ERRNO; - *errnop = ERANGE; - return NSS_STATUS_TRYAGAIN; + *errnop = e; + return status; } - memcpy(buffer, translated, l); - - pwd->pw_name = buffer; - pwd->pw_uid = uid; - pwd->pw_gid = uid; - pwd->pw_gecos = (char*) DYNAMIC_USER_GECOS; - pwd->pw_passwd = (char*) DYNAMIC_USER_PASSWD; - pwd->pw_dir = (char*) DYNAMIC_USER_DIR; - pwd->pw_shell = (char*) DYNAMIC_USER_SHELL; - - return NSS_STATUS_SUCCESS; - -fail: - UNPROTECT_ERRNO; - *errnop = -r; - return NSS_STATUS_UNAVAIL; + return status; } #pragma GCC diagnostic ignored "-Wsizeof-pointer-memaccess" @@ -344,94 +182,46 @@ enum nss_status _nss_systemd_getgrnam_r( char *buffer, size_t buflen, int *errnop) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL; - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - uint32_t translated; - size_t l; - int bypass, r; + enum nss_status status; + int e; PROTECT_ERRNO; BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); assert(name); assert(gr); + assert(errnop); if (!valid_user_group_name(name)) return NSS_STATUS_NOTFOUND; /* Synthesize records for root and nobody, in case they are missing form /etc/group */ if (getenv_bool_secure("SYSTEMD_NSS_BYPASS_SYNTHETIC") <= 0) { + if (streq(name, root_group.gr_name)) { *gr = root_group; return NSS_STATUS_SUCCESS; } - if (synthesize_nobody() && - streq(name, nobody_group.gr_name)) { + + if (streq(name, nobody_group.gr_name)) { + if (!synthesize_nobody()) + return NSS_STATUS_NOTFOUND; + *gr = nobody_group; return NSS_STATUS_SUCCESS; } - } - if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0) + } else if (STR_IN_SET(name, root_group.gr_name, nobody_group.gr_name)) return NSS_STATUS_NOTFOUND; - bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS"); - if (bypass <= 0) { - r = sd_bus_open_system(&bus); - if (r < 0) - bypass = 1; - } - - if (bypass > 0) { - r = direct_lookup_name(name, (uid_t*) &translated); - if (r == -ENOENT) - return NSS_STATUS_NOTFOUND; - if (r < 0) - goto fail; - } else { - r = sd_bus_call_method(bus, - "org.freedesktop.systemd1", - "/org/freedesktop/systemd1", - "org.freedesktop.systemd1.Manager", - "LookupDynamicUserByName", - &error, - &reply, - "s", - name); - if (r < 0) { - if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_DYNAMIC_USER)) - return NSS_STATUS_NOTFOUND; - - goto fail; - } - - r = sd_bus_message_read(reply, "u", &translated); - if (r < 0) - goto fail; - } - - l = sizeof(char*) + strlen(name) + 1; - if (buflen < l) { + status = userdb_getgrnam(name, gr, buffer, buflen, &e); + if (IN_SET(status, NSS_STATUS_UNAVAIL, NSS_STATUS_TRYAGAIN)) { UNPROTECT_ERRNO; - *errnop = ERANGE; - return NSS_STATUS_TRYAGAIN; + *errnop = e; + return status; } - memzero(buffer, sizeof(char*)); - strcpy(buffer + sizeof(char*), name); - - gr->gr_name = buffer + sizeof(char*); - gr->gr_gid = (gid_t) translated; - gr->gr_passwd = (char*) DYNAMIC_USER_PASSWD; - gr->gr_mem = (char**) buffer; - - return NSS_STATUS_SUCCESS; - -fail: - UNPROTECT_ERRNO; - *errnop = -r; - return NSS_STATUS_UNAVAIL; + return status; } enum nss_status _nss_systemd_getgrgid_r( @@ -440,154 +230,56 @@ enum nss_status _nss_systemd_getgrgid_r( char *buffer, size_t buflen, int *errnop) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL; - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - _cleanup_free_ char *direct = NULL; - const char *translated; - size_t l; - int bypass, r; + enum nss_status status; + int e; PROTECT_ERRNO; BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); + assert(gr); + assert(errnop); + if (!gid_is_valid(gid)) return NSS_STATUS_NOTFOUND; /* Synthesize records for root and nobody, in case they are missing from /etc/group */ if (getenv_bool_secure("SYSTEMD_NSS_BYPASS_SYNTHETIC") <= 0) { + if (gid == root_group.gr_gid) { *gr = root_group; return NSS_STATUS_SUCCESS; } - if (synthesize_nobody() && - gid == nobody_group.gr_gid) { + + if (gid == nobody_group.gr_gid) { + if (!synthesize_nobody()) + return NSS_STATUS_NOTFOUND; + *gr = nobody_group; return NSS_STATUS_SUCCESS; } - } - if (!gid_is_dynamic(gid)) + } else if (gid == root_group.gr_gid || gid == nobody_group.gr_gid) return NSS_STATUS_NOTFOUND; - if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0) - return NSS_STATUS_NOTFOUND; - - bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS"); - if (bypass <= 0) { - r = sd_bus_open_system(&bus); - if (r < 0) - bypass = 1; - } - - if (bypass > 0) { - r = direct_lookup_uid(gid, &direct); - if (r == -ENOENT) - return NSS_STATUS_NOTFOUND; - if (r < 0) - goto fail; - - translated = direct; - - } else { - r = sd_bus_call_method(bus, - "org.freedesktop.systemd1", - "/org/freedesktop/systemd1", - "org.freedesktop.systemd1.Manager", - "LookupDynamicUserByUID", - &error, - &reply, - "u", - (uint32_t) gid); - if (r < 0) { - if (sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_DYNAMIC_USER)) - return NSS_STATUS_NOTFOUND; - - goto fail; - } - - r = sd_bus_message_read(reply, "s", &translated); - if (r < 0) - goto fail; - } - - l = sizeof(char*) + strlen(translated) + 1; - if (buflen < l) { + status = userdb_getgrgid(gid, gr, buffer, buflen, &e); + if (IN_SET(status, NSS_STATUS_UNAVAIL, NSS_STATUS_TRYAGAIN)) { UNPROTECT_ERRNO; - *errnop = ERANGE; - return NSS_STATUS_TRYAGAIN; + *errnop = e; + return status; } - memzero(buffer, sizeof(char*)); - strcpy(buffer + sizeof(char*), translated); - - gr->gr_name = buffer + sizeof(char*); - gr->gr_gid = gid; - gr->gr_passwd = (char*) DYNAMIC_USER_PASSWD; - gr->gr_mem = (char**) buffer; - - return NSS_STATUS_SUCCESS; - -fail: - UNPROTECT_ERRNO; - *errnop = -r; - return NSS_STATUS_UNAVAIL; -} - -static void user_entry_free(UserEntry *p) { - if (!p) - return; - - if (p->data) - LIST_REMOVE(entries, p->data->entries, p); - - free(p->name); - free(p); -} - -static int user_entry_add(GetentData *data, const char *name, uid_t id) { - UserEntry *p; - - assert(data); - - /* This happens when User= or Group= already exists statically. */ - if (!uid_is_dynamic(id)) - return -EINVAL; - - p = new0(UserEntry, 1); - if (!p) - return -ENOMEM; - - p->name = strdup(name); - if (!p->name) { - free(p); - return -ENOMEM; - } - p->id = id; - p->data = data; - - LIST_PREPEND(entries, data->entries, p); - - return 0; -} - -static void systemd_endent(GetentData *data) { - UserEntry *p; - - assert(data); - - while ((p = data->entries)) - user_entry_free(p); - - data->position = NULL; + return status; } static enum nss_status nss_systemd_endent(GetentData *p) { PROTECT_ERRNO; BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); + assert(p); + assert_se(pthread_mutex_lock(&p->mutex) == 0); - systemd_endent(p); + p->iterator = userdb_iterator_free(p->iterator); + p->by_membership = false; assert_se(pthread_mutex_unlock(&p->mutex) == 0); return NSS_STATUS_SUCCESS; @@ -601,235 +293,353 @@ enum nss_status _nss_systemd_endgrent(void) { return nss_systemd_endent(&getgrent_data); } -static int direct_enumeration(GetentData *p) { - _cleanup_closedir_ DIR *d = NULL; - struct dirent *de; - int r; - - assert(p); - - d = opendir("/run/systemd/dynamic-uid/"); - if (!d) - return -errno; - - FOREACH_DIRENT(de, d, return -errno) { - _cleanup_free_ char *name = NULL; - uid_t uid, verified; - - if (!dirent_is_file(de)) - continue; - - r = parse_uid(de->d_name, &uid); - if (r < 0) - continue; - - r = direct_lookup_uid(uid, &name); - if (r == -ENOMEM) - return r; - if (r < 0) - continue; - - r = direct_lookup_name(name, &verified); - if (r < 0) - continue; - - if (uid != verified) - continue; - - r = user_entry_add(p, name, uid); - if (r == -ENOMEM) - return r; - if (r < 0) - continue; - } - - return 0; -} - -static enum nss_status systemd_setent(GetentData *p) { - _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; - _cleanup_(sd_bus_message_unrefp) sd_bus_message* reply = NULL; - _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - const char *name; - uid_t id; - int bypass, r; - - PROTECT_ERRNO; - BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); - - assert(p); - - assert_se(pthread_mutex_lock(&p->mutex) == 0); - - systemd_endent(p); - - if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0) - goto finish; - - bypass = getenv_bool_secure("SYSTEMD_NSS_BYPASS_BUS"); - - if (bypass <= 0) { - r = sd_bus_open_system(&bus); - if (r < 0) - bypass = 1; - } - - if (bypass > 0) { - r = direct_enumeration(p); - if (r < 0) - goto fail; - - goto finish; - } - - r = sd_bus_call_method(bus, - "org.freedesktop.systemd1", - "/org/freedesktop/systemd1", - "org.freedesktop.systemd1.Manager", - "GetDynamicUsers", - &error, - &reply, - NULL); - if (r < 0) - goto fail; - - r = sd_bus_message_enter_container(reply, 'a', "(us)"); - if (r < 0) - goto fail; - - while ((r = sd_bus_message_read(reply, "(us)", &id, &name)) > 0) { - r = user_entry_add(p, name, id); - if (r == -ENOMEM) - goto fail; - if (r < 0) - continue; - } - if (r < 0) - goto fail; - - r = sd_bus_message_exit_container(reply); - if (r < 0) - goto fail; - -finish: - p->position = p->entries; - assert_se(pthread_mutex_unlock(&p->mutex) == 0); - - return NSS_STATUS_SUCCESS; - -fail: - systemd_endent(p); - assert_se(pthread_mutex_unlock(&p->mutex) == 0); - - return NSS_STATUS_UNAVAIL; -} - enum nss_status _nss_systemd_setpwent(int stayopen) { - return systemd_setent(&getpwent_data); -} - -enum nss_status _nss_systemd_setgrent(int stayopen) { - return systemd_setent(&getgrent_data); -} - -enum nss_status _nss_systemd_getpwent_r(struct passwd *result, char *buffer, size_t buflen, int *errnop) { enum nss_status ret; - UserEntry *p; - size_t len; PROTECT_ERRNO; BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); - assert(result); - assert(buffer); - assert(errnop); + if (userdb_nss_compat_is_enabled() <= 0) + return NSS_STATUS_NOTFOUND; assert_se(pthread_mutex_lock(&getpwent_data.mutex) == 0); - LIST_FOREACH(entries, p, getpwent_data.position) { - len = strlen(p->name) + 1; - if (buflen < len) { - UNPROTECT_ERRNO; - *errnop = ERANGE; - ret = NSS_STATUS_TRYAGAIN; - goto finalize; - } + getpwent_data.iterator = userdb_iterator_free(getpwent_data.iterator); + getpwent_data.by_membership = false; - memcpy(buffer, p->name, len); - - result->pw_name = buffer; - result->pw_uid = p->id; - result->pw_gid = p->id; - result->pw_gecos = (char*) DYNAMIC_USER_GECOS; - result->pw_passwd = (char*) DYNAMIC_USER_PASSWD; - result->pw_dir = (char*) DYNAMIC_USER_DIR; - result->pw_shell = (char*) DYNAMIC_USER_SHELL; - break; - } - if (!p) { - ret = NSS_STATUS_NOTFOUND; - goto finalize; - } - - /* On success, step to the next entry. */ - p = p->entries_next; - ret = NSS_STATUS_SUCCESS; - -finalize: - /* Save position for the next call. */ - getpwent_data.position = p; + ret = userdb_all(nss_glue_userdb_flags(), &getpwent_data.iterator) < 0 ? + NSS_STATUS_UNAVAIL : NSS_STATUS_SUCCESS; assert_se(pthread_mutex_unlock(&getpwent_data.mutex) == 0); - return ret; } -enum nss_status _nss_systemd_getgrent_r(struct group *result, char *buffer, size_t buflen, int *errnop) { +enum nss_status _nss_systemd_setgrent(int stayopen) { enum nss_status ret; - UserEntry *p; - size_t len; + + PROTECT_ERRNO; + BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); + + if (userdb_nss_compat_is_enabled() <= 0) + return NSS_STATUS_NOTFOUND; + + assert_se(pthread_mutex_lock(&getgrent_data.mutex) == 0); + + getgrent_data.iterator = userdb_iterator_free(getgrent_data.iterator); + getpwent_data.by_membership = false; + + ret = groupdb_all(nss_glue_userdb_flags(), &getgrent_data.iterator) < 0 ? + NSS_STATUS_UNAVAIL : NSS_STATUS_SUCCESS; + + assert_se(pthread_mutex_unlock(&getgrent_data.mutex) == 0); + return ret; +} + +enum nss_status _nss_systemd_getpwent_r( + struct passwd *result, + char *buffer, size_t buflen, + int *errnop) { + + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + enum nss_status ret; + int r; PROTECT_ERRNO; BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); assert(result); - assert(buffer); assert(errnop); + r = userdb_nss_compat_is_enabled(); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + if (!r) + return NSS_STATUS_NOTFOUND; + + assert_se(pthread_mutex_lock(&getpwent_data.mutex) == 0); + + if (!getpwent_data.iterator) { + UNPROTECT_ERRNO; + *errnop = EHOSTDOWN; + ret = NSS_STATUS_UNAVAIL; + goto finish; + } + + r = userdb_iterator_get(getpwent_data.iterator, &ur); + if (r == -ESRCH) { + ret = NSS_STATUS_NOTFOUND; + goto finish; + } + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + ret = NSS_STATUS_UNAVAIL; + goto finish; + } + + r = nss_pack_user_record(ur, result, buffer, buflen); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + ret = NSS_STATUS_TRYAGAIN; + goto finish; + } + + ret = NSS_STATUS_SUCCESS; + +finish: + assert_se(pthread_mutex_unlock(&getpwent_data.mutex) == 0); + return ret; +} + +enum nss_status _nss_systemd_getgrent_r( + struct group *result, + char *buffer, size_t buflen, + int *errnop) { + + _cleanup_(group_record_unrefp) GroupRecord *gr = NULL; + _cleanup_free_ char **members = NULL; + enum nss_status ret; + int r; + + PROTECT_ERRNO; + BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); + + assert(result); + assert(errnop); + + r = userdb_nss_compat_is_enabled(); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + if (!r) + return NSS_STATUS_UNAVAIL; + assert_se(pthread_mutex_lock(&getgrent_data.mutex) == 0); - LIST_FOREACH(entries, p, getgrent_data.position) { - len = sizeof(char*) + strlen(p->name) + 1; - if (buflen < len) { + if (!getgrent_data.iterator) { + UNPROTECT_ERRNO; + *errnop = EHOSTDOWN; + ret = NSS_STATUS_UNAVAIL; + goto finish; + } + + if (!getgrent_data.by_membership) { + r = groupdb_iterator_get(getgrent_data.iterator, &gr); + if (r == -ESRCH) { + /* So we finished iterating native groups now. let's now continue with iterating + * native memberships, and generate additional group entries for any groups + * referenced there that are defined in NSS only. This means for those groups there + * will be two or more entries generated during iteration, but this is apparently how + * this is supposed to work, and what other implementations do too. Clients are + * supposed to merge the group records found during iteration automatically. */ + getgrent_data.iterator = userdb_iterator_free(getgrent_data.iterator); + + r = membershipdb_all(nss_glue_userdb_flags(), &getgrent_data.iterator); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + ret = NSS_STATUS_UNAVAIL; + goto finish; + } + + getgrent_data.by_membership = true; + } else if (r < 0) { UNPROTECT_ERRNO; - *errnop = ERANGE; - ret = NSS_STATUS_TRYAGAIN; - goto finalize; + *errnop = -r; + ret = NSS_STATUS_UNAVAIL; + goto finish; + } else if (!STR_IN_SET(gr->group_name, root_group.gr_name, nobody_group.gr_name)) { + r = membershipdb_by_group_strv(gr->group_name, nss_glue_userdb_flags(), &members); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + ret = NSS_STATUS_UNAVAIL; + goto finish; + } } - - memzero(buffer, sizeof(char*)); - strcpy(buffer + sizeof(char*), p->name); - - result->gr_name = buffer + sizeof(char*); - result->gr_gid = p->id; - result->gr_passwd = (char*) DYNAMIC_USER_PASSWD; - result->gr_mem = (char**) buffer; - break; - } - if (!p) { - ret = NSS_STATUS_NOTFOUND; - goto finalize; } - /* On success, step to the next entry. */ - p = p->entries_next; + if (getgrent_data.by_membership) { + _cleanup_close_ int lock_fd = -1; + + for (;;) { + _cleanup_free_ char *user_name = NULL, *group_name = NULL; + + r = membershipdb_iterator_get(getgrent_data.iterator, &user_name, &group_name); + if (r == -ESRCH) { + ret = NSS_STATUS_NOTFOUND; + goto finish; + } + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + ret = NSS_STATUS_UNAVAIL; + goto finish; + } + + if (STR_IN_SET(user_name, root_passwd.pw_name, nobody_passwd.pw_name)) + continue; + if (STR_IN_SET(group_name, root_group.gr_name, nobody_group.gr_name)) + continue; + + /* We are about to recursively call into NSS, let's make sure we disable recursion into our own code. */ + if (lock_fd < 0) { + lock_fd = userdb_nss_compat_disable(); + if (lock_fd < 0 && lock_fd != -EBUSY) { + UNPROTECT_ERRNO; + *errnop = -lock_fd; + ret = NSS_STATUS_UNAVAIL; + goto finish; + } + } + + r = nss_group_record_by_name(group_name, &gr); + if (r == -ESRCH) + continue; + if (r < 0) { + log_debug_errno(r, "Failed to do NSS check for group '%s', ignoring: %m", group_name); + continue; + } + + members = strv_new(user_name); + if (!members) { + UNPROTECT_ERRNO; + *errnop = ENOMEM; + return NSS_STATUS_TRYAGAIN; + } + + /* Note that we currently generate one group entry per user that is part of a + * group. It's a bit ugly, but equivalent to generating a single entry with a set of + * members in them. */ + break; + } + } + + r = nss_pack_group_record(gr, members, result, buffer, buflen); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + ret = NSS_STATUS_TRYAGAIN; + goto finish; + } + ret = NSS_STATUS_SUCCESS; -finalize: - /* Save position for the next call. */ - getgrent_data.position = p; - +finish: assert_se(pthread_mutex_unlock(&getgrent_data.mutex) == 0); - return ret; } + +enum nss_status _nss_systemd_initgroups_dyn( + const char *user_name, + gid_t gid, + long *start, + long *size, + gid_t **groupsp, + long int limit, + int *errnop) { + + _cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL; + bool any = false; + int r; + + PROTECT_ERRNO; + BLOCK_SIGNALS(NSS_SIGNALS_BLOCK); + + assert(user_name); + assert(start); + assert(size); + assert(groupsp); + assert(errnop); + + if (!valid_user_group_name(user_name)) + return NSS_STATUS_NOTFOUND; + + /* Don't allow extending these two special users, the same as we won't resolve them via getpwnam() */ + if (STR_IN_SET(user_name, root_passwd.pw_name, nobody_passwd.pw_name)) + return NSS_STATUS_NOTFOUND; + + r = userdb_nss_compat_is_enabled(); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + if (!r) + return NSS_STATUS_NOTFOUND; + + r = membershipdb_by_user(user_name, nss_glue_userdb_flags(), &iterator); + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + + for (;;) { + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; + _cleanup_free_ char *group_name = NULL; + + r = membershipdb_iterator_get(iterator, NULL, &group_name); + if (r == -ESRCH) + break; + if (r < 0) { + UNPROTECT_ERRNO; + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + + /* The group might be defined via traditional NSS only, hence let's do a full look-up without + * disabling NSS. This means we are operating recursively here. */ + + r = groupdb_by_name(group_name, nss_glue_userdb_flags() & ~USERDB_AVOID_NSS, &g); + if (r == -ESRCH) + continue; + if (r < 0) { + log_debug_errno(r, "Failed to resolve group '%s', ignoring: %m", group_name); + continue; + } + + if (g->gid == gid) + continue; + + if (*start >= *size) { + gid_t *new_groups; + long new_size; + + if (limit > 0 && *size >= limit) /* Reached the limit.? */ + break; + + if (*size > LONG_MAX/2) { /* Check for overflow */ + UNPROTECT_ERRNO; + *errnop = ENOMEM; + return NSS_STATUS_TRYAGAIN; + } + + new_size = *start * 2; + if (limit > 0 && new_size > limit) + new_size = limit; + + /* Enlarge buffer */ + new_groups = realloc(*groupsp, new_size * sizeof(**groupsp)); + if (!new_groups) { + UNPROTECT_ERRNO; + *errnop = ENOMEM; + return NSS_STATUS_TRYAGAIN; + } + + *groupsp = new_groups; + *size = new_size; + } + + (*groupsp)[(*start)++] = g->gid; + any = true; + } + + return any ? NSS_STATUS_SUCCESS : NSS_STATUS_NOTFOUND; +} diff --git a/src/nss-systemd/nss-systemd.sym b/src/nss-systemd/nss-systemd.sym index ff63382b15..77e1fbe93f 100644 --- a/src/nss-systemd/nss-systemd.sym +++ b/src/nss-systemd/nss-systemd.sym @@ -19,5 +19,6 @@ global: _nss_systemd_endgrent; _nss_systemd_setgrent; _nss_systemd_getgrent_r; + _nss_systemd_initgroups_dyn; local: *; }; diff --git a/src/nss-systemd/userdb-glue.c b/src/nss-systemd/userdb-glue.c new file mode 100644 index 0000000000..81705fa1b6 --- /dev/null +++ b/src/nss-systemd/userdb-glue.c @@ -0,0 +1,344 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "env-util.h" +#include "fd-util.h" +#include "group-record-nss.h" +#include "strv.h" +#include "user-record.h" +#include "userdb-glue.h" +#include "userdb.h" + +UserDBFlags nss_glue_userdb_flags(void) { + UserDBFlags flags = USERDB_AVOID_NSS; + + /* Make sure that we don't go in circles when allocating a dynamic UID by checking our own database */ + if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0) + flags |= USERDB_AVOID_DYNAMIC_USER; + + return flags; +} + +int nss_pack_user_record( + UserRecord *hr, + struct passwd *pwd, + char *buffer, + size_t buflen) { + + const char *rn, *hd, *shell; + size_t required; + + assert(hr); + assert(pwd); + + assert_se(hr->user_name); + required = strlen(hr->user_name) + 1; + + assert_se(rn = user_record_real_name(hr)); + required += strlen(rn) + 1; + + assert_se(hd = user_record_home_directory(hr)); + required += strlen(hd) + 1; + + assert_se(shell = user_record_shell(hr)); + required += strlen(shell) + 1; + + if (buflen < required) + return -ERANGE; + + *pwd = (struct passwd) { + .pw_name = buffer, + .pw_uid = hr->uid, + .pw_gid = user_record_gid(hr), + .pw_passwd = (char*) "x", /* means: see shadow file */ + }; + + assert(buffer); + + pwd->pw_gecos = stpcpy(pwd->pw_name, hr->user_name) + 1; + pwd->pw_dir = stpcpy(pwd->pw_gecos, rn) + 1; + pwd->pw_shell = stpcpy(pwd->pw_dir, hd) + 1; + strcpy(pwd->pw_shell, shell); + + return 0; +} + +enum nss_status userdb_getpwnam( + const char *name, + struct passwd *pwd, + char *buffer, size_t buflen, + int *errnop) { + + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + int r; + + assert(pwd); + assert(errnop); + + r = userdb_nss_compat_is_enabled(); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + if (!r) + return NSS_STATUS_NOTFOUND; + + r = userdb_by_name(name, nss_glue_userdb_flags(), &hr); + if (r == -ESRCH) + return NSS_STATUS_NOTFOUND; + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + + r = nss_pack_user_record(hr, pwd, buffer, buflen); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_TRYAGAIN; + } + + return NSS_STATUS_SUCCESS; +} + +enum nss_status userdb_getpwuid( + uid_t uid, + struct passwd *pwd, + char *buffer, + size_t buflen, + int *errnop) { + + _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + int r; + + assert(pwd); + assert(errnop); + + r = userdb_nss_compat_is_enabled(); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + if (!r) + return NSS_STATUS_NOTFOUND; + + r = userdb_by_uid(uid, nss_glue_userdb_flags(), &hr); + if (r == -ESRCH) + return NSS_STATUS_NOTFOUND; + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + + r = nss_pack_user_record(hr, pwd, buffer, buflen); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_TRYAGAIN; + } + + return NSS_STATUS_SUCCESS; +} + +int nss_pack_group_record( + GroupRecord *g, + char **extra_members, + struct group *gr, + char *buffer, + size_t buflen) { + + char **array = NULL, *p, **m; + size_t required, n = 0, i = 0; + + assert(g); + assert(gr); + + assert_se(g->group_name); + required = strlen(g->group_name) + 1; + + STRV_FOREACH(m, g->members) { + required += sizeof(char*); /* space for ptr array entry */ + required += strlen(*m) + 1; + n++; + } + STRV_FOREACH(m, extra_members) { + if (strv_contains(g->members, *m)) + continue; + + required += sizeof(char*); + required += strlen(*m) + 1; + n++; + } + + required += sizeof(char*); /* trailing NULL in ptr array entry */ + + if (buflen < required) + return -ERANGE; + + array = (char**) buffer; /* place ptr array at beginning of buffer, under assumption buffer is aligned */ + p = buffer + sizeof(void*) * (n + 1); /* place member strings right after the ptr array */ + + STRV_FOREACH(m, g->members) { + array[i++] = p; + p = stpcpy(p, *m) + 1; + } + STRV_FOREACH(m, extra_members) { + if (strv_contains(g->members, *m)) + continue; + + array[i++] = p; + p = stpcpy(p, *m) + 1; + } + + assert_se(i == n); + array[n] = NULL; + + *gr = (struct group) { + .gr_name = strcpy(p, g->group_name), + .gr_gid = g->gid, + .gr_passwd = (char*) "x", /* means: see shadow file */ + .gr_mem = array, + }; + + return 0; +} + +enum nss_status userdb_getgrnam( + const char *name, + struct group *gr, + char *buffer, + size_t buflen, + int *errnop) { + + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; + _cleanup_strv_free_ char **members = NULL; + int r; + + assert(gr); + assert(errnop); + + r = userdb_nss_compat_is_enabled(); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + if (!r) + return NSS_STATUS_NOTFOUND; + + r = groupdb_by_name(name, nss_glue_userdb_flags(), &g); + if (r < 0 && r != -ESRCH) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + + r = membershipdb_by_group_strv(name, nss_glue_userdb_flags(), &members); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + + if (!g) { + _cleanup_close_ int lock_fd = -1; + + if (strv_isempty(members)) + return NSS_STATUS_NOTFOUND; + + /* Grmbl, so we are supposed to extend a group entry, but the group entry itself is not + * accessible via non-NSS. Hence let's do what we have to do, and query NSS after all to + * acquire it, so that we can extend it (that's because glibc's group merging feature will + * merge groups only if both GID and name match and thus we need to have both first). It + * sucks behaving recursively likely this, but it's apparently what everybody does. We break + * the recursion for ourselves via the userdb_nss_compat_disable() lock. */ + + lock_fd = userdb_nss_compat_disable(); + if (lock_fd < 0 && lock_fd != -EBUSY) + return lock_fd; + + r = nss_group_record_by_name(name, &g); + if (r == -ESRCH) + return NSS_STATUS_NOTFOUND; + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + } + + r = nss_pack_group_record(g, members, gr, buffer, buflen); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_TRYAGAIN; + } + + return NSS_STATUS_SUCCESS; +} + +enum nss_status userdb_getgrgid( + gid_t gid, + struct group *gr, + char *buffer, + size_t buflen, + int *errnop) { + + + _cleanup_(group_record_unrefp) GroupRecord *g = NULL; + _cleanup_strv_free_ char **members = NULL; + bool from_nss; + int r; + + assert(gr); + assert(errnop); + + r = userdb_nss_compat_is_enabled(); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + if (r) + return NSS_STATUS_NOTFOUND; + + r = groupdb_by_gid(gid, nss_glue_userdb_flags(), &g); + if (r < 0 && r != -ESRCH) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + + if (!g) { + _cleanup_close_ int lock_fd = -1; + + /* So, quite possibly we have to extend an existing group record with additional members. But + * to do this we need to know the group name first. The group didn't exist via non-NSS + * queries though, hence let's try to acquire it here recursively via NSS. */ + + lock_fd = userdb_nss_compat_disable(); + if (lock_fd < 0 && lock_fd != -EBUSY) + return lock_fd; + + r = nss_group_record_by_gid(gid, &g); + if (r == -ESRCH) + return NSS_STATUS_NOTFOUND; + + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + + from_nss = true; + } else + from_nss = false; + + r = membershipdb_by_group_strv(g->group_name, nss_glue_userdb_flags(), &members); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_UNAVAIL; + } + + /* If we acquired the record via NSS then there's no reason to respond unless we have to agument the + * list of members of the group */ + if (from_nss && strv_isempty(members)) + return NSS_STATUS_NOTFOUND; + + r = nss_pack_group_record(g, members, gr, buffer, buflen); + if (r < 0) { + *errnop = -r; + return NSS_STATUS_TRYAGAIN; + } + + return NSS_STATUS_SUCCESS; +} diff --git a/src/nss-systemd/userdb-glue.h b/src/nss-systemd/userdb-glue.h new file mode 100644 index 0000000000..02add24b6b --- /dev/null +++ b/src/nss-systemd/userdb-glue.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include +#include +#include +#include + +#include "userdb.h" + +UserDBFlags nss_glue_userdb_flags(void); + +int nss_pack_user_record(UserRecord *hr, struct passwd *pwd, char *buffer, size_t buflen); +int nss_pack_group_record(GroupRecord *g, char **extra_members, struct group *gr, char *buffer, size_t buflen); + +enum nss_status userdb_getpwnam(const char *name, struct passwd *pwd, char *buffer, size_t buflen, int *errnop); +enum nss_status userdb_getpwuid(uid_t uid, struct passwd *pwd, char *buffer, size_t buflen, int *errnop); + +enum nss_status userdb_getgrnam(const char *name, struct group *gr, char *buffer, size_t buflen, int *errnop); +enum nss_status userdb_getgrgid(gid_t gid, struct group *gr, char *buffer, size_t buflen, int *errnop); From 22c902faccb3840350de6534776351131de95daa Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 7 Aug 2019 16:22:35 +0200 Subject: [PATCH 13/29] logind: port to UserRecord object This changes the user tracking of logind to use the new-style UserRecord object. In a later commit this enables us to do per-user resource management. --- src/login/logind-core.c | 39 ++++++++++--------- src/login/logind-dbus.c | 10 ++--- src/login/logind-seat.c | 8 ++-- src/login/logind-session-dbus.c | 22 +++++------ src/login/logind-session.c | 20 +++++----- src/login/logind-user-dbus.c | 68 +++++++++++++++++++++++++++++---- src/login/logind-user.c | 58 ++++++++++++---------------- src/login/logind-user.h | 10 ++--- src/login/logind.h | 5 ++- 9 files changed, 144 insertions(+), 96 deletions(-) diff --git a/src/login/logind-core.c b/src/login/logind-core.c index e84b38096a..22a42b077c 100644 --- a/src/login/logind-core.c +++ b/src/login/logind-core.c @@ -27,6 +27,7 @@ #include "terminal-util.h" #include "udev-util.h" #include "user-util.h" +#include "userdb.h" void manager_reset_config(Manager *m) { assert(m); @@ -137,21 +138,18 @@ int manager_add_session(Manager *m, const char *id, Session **ret_session) { int manager_add_user( Manager *m, - uid_t uid, - gid_t gid, - const char *name, - const char *home, + UserRecord *ur, User **ret_user) { User *u; int r; assert(m); - assert(name); + assert(ur); - u = hashmap_get(m->users, UID_TO_PTR(uid)); + u = hashmap_get(m->users, UID_TO_PTR(ur->uid)); if (!u) { - r = user_new(&u, m, uid, gid, name, home); + r = user_new(&u, m, ur); if (r < 0) return r; } @@ -167,32 +165,35 @@ int manager_add_user_by_name( const char *name, User **ret_user) { - const char *home = NULL; - uid_t uid; - gid_t gid; + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; int r; assert(m); assert(name); - r = get_user_creds(&name, &uid, &gid, &home, NULL, 0); + r = userdb_by_name(name, 0, &ur); if (r < 0) return r; - return manager_add_user(m, uid, gid, name, home, ret_user); + return manager_add_user(m, ur, ret_user); } -int manager_add_user_by_uid(Manager *m, uid_t uid, User **ret_user) { - struct passwd *p; +int manager_add_user_by_uid( + Manager *m, + uid_t uid, + User **ret_user) { + + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + int r; assert(m); + assert(uid_is_valid(uid)); - errno = 0; - p = getpwuid(uid); - if (!p) - return errno_or_else(ENOENT); + r = userdb_by_uid(uid, 0, &ur); + if (r < 0) + return r; - return manager_add_user(m, uid, p->pw_gid, p->pw_name, p->pw_dir, ret_user); + return manager_add_user(m, ur, ret_user); } int manager_add_inhibitor(Manager *m, const char* id, Inhibitor **ret) { diff --git a/src/login/logind-dbus.c b/src/login/logind-dbus.c index 69b5994878..d3aa6815a6 100644 --- a/src/login/logind-dbus.c +++ b/src/login/logind-dbus.c @@ -536,8 +536,8 @@ static int method_list_sessions(sd_bus_message *message, void *userdata, sd_bus_ r = sd_bus_message_append(reply, "(susso)", session->id, - (uint32_t) session->user->uid, - session->user->name, + (uint32_t) session->user->user_record->uid, + session->user->user_record->user_name, session->seat ? session->seat->id : "", p); if (r < 0) @@ -577,8 +577,8 @@ static int method_list_users(sd_bus_message *message, void *userdata, sd_bus_err return -ENOMEM; r = sd_bus_message_append(reply, "(uso)", - (uint32_t) user->uid, - user->name, + (uint32_t) user->user_record->uid, + user->user_record->user_name, p); if (r < 0) return r; @@ -1488,7 +1488,7 @@ static int have_multiple_sessions( * count, and non-login sessions do not count either. */ HASHMAP_FOREACH(session, m->sessions, i) if (session->class == SESSION_USER && - session->user->uid != uid) + session->user->user_record->uid != uid) return true; return false; diff --git a/src/login/logind-seat.c b/src/login/logind-seat.c index 026bcf4d8e..394f89dd7e 100644 --- a/src/login/logind-seat.c +++ b/src/login/logind-seat.c @@ -119,7 +119,7 @@ int seat_save(Seat *s) { "ACTIVE=%s\n" "ACTIVE_UID="UID_FMT"\n", s->active->id, - s->active->user->uid); + s->active->user->user_record->uid); } if (s->sessions) { @@ -137,7 +137,7 @@ int seat_save(Seat *s) { LIST_FOREACH(sessions_by_seat, i, s->sessions) fprintf(f, UID_FMT"%c", - i->user->uid, + i->user->user_record->uid, i->sessions_by_seat_next ? ' ' : '\n'); } @@ -216,8 +216,8 @@ int seat_apply_acls(Seat *s, Session *old_active) { r = devnode_acl_all(s->id, false, - !!old_active, old_active ? old_active->user->uid : 0, - !!s->active, s->active ? s->active->user->uid : 0); + !!old_active, old_active ? old_active->user->user_record->uid : 0, + !!s->active, s->active ? s->active->user->user_record->uid : 0); if (r < 0) return log_error_errno(r, "Failed to apply ACLs: %m"); diff --git a/src/login/logind-session-dbus.c b/src/login/logind-session-dbus.c index 32a64fff0c..80f3e432de 100644 --- a/src/login/logind-session-dbus.c +++ b/src/login/logind-session-dbus.c @@ -43,7 +43,7 @@ static int property_get_user( if (!p) return -ENOMEM; - return sd_bus_message_append(reply, "(uo)", (uint32_t) s->user->uid, p); + return sd_bus_message_append(reply, "(uo)", (uint32_t) s->user->user_record->uid, p); } static int property_get_name( @@ -61,7 +61,7 @@ static int property_get_name( assert(reply); assert(s); - return sd_bus_message_append(reply, "s", s->user->name); + return sd_bus_message_append(reply, "s", s->user->user_record->user_name); } static int property_get_seat( @@ -168,7 +168,7 @@ int bus_session_method_terminate(sd_bus_message *message, void *userdata, sd_bus "org.freedesktop.login1.manage", NULL, false, - s->user->uid, + s->user->user_record->uid, &s->manager->polkit_registry, error); if (r < 0) @@ -210,7 +210,7 @@ int bus_session_method_lock(sd_bus_message *message, void *userdata, sd_bus_erro "org.freedesktop.login1.lock-sessions", NULL, false, - s->user->uid, + s->user->user_record->uid, &s->manager->polkit_registry, error); if (r < 0) @@ -246,7 +246,7 @@ static int method_set_idle_hint(sd_bus_message *message, void *userdata, sd_bus_ if (r < 0) return r; - if (uid != 0 && uid != s->user->uid) + if (uid != 0 && uid != s->user->user_record->uid) return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may set idle hint"); r = session_set_idle_hint(s, b); @@ -279,7 +279,7 @@ static int method_set_locked_hint(sd_bus_message *message, void *userdata, sd_bu if (r < 0) return r; - if (uid != 0 && uid != s->user->uid) + if (uid != 0 && uid != s->user->user_record->uid) return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may set locked hint"); session_set_locked_hint(s, b); @@ -318,7 +318,7 @@ int bus_session_method_kill(sd_bus_message *message, void *userdata, sd_bus_erro "org.freedesktop.login1.manage", NULL, false, - s->user->uid, + s->user->user_record->uid, &s->manager->polkit_registry, error); if (r < 0) @@ -354,7 +354,7 @@ static int method_take_control(sd_bus_message *message, void *userdata, sd_bus_e if (r < 0) return r; - if (uid != 0 && (force || uid != s->user->uid)) + if (uid != 0 && (force || uid != s->user->user_record->uid)) return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may take control"); r = session_set_controller(s, sd_bus_message_get_sender(message), force, true); @@ -523,7 +523,7 @@ static int method_set_brightness(sd_bus_message *message, void *userdata, sd_bus if (r < 0) return r; - if (uid != 0 && uid != s->user->uid) + if (uid != 0 && uid != s->user->user_record->uid) return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may change brightness."); r = sd_device_new_from_subsystem_sysname(&d, subsystem, name); @@ -822,7 +822,7 @@ int session_send_create_reply(Session *s, sd_bus_error *error) { "session_fd=%d seat=%s vtnr=%u", s->id, p, - (uint32_t) s->user->uid, + (uint32_t) s->user->user_record->uid, s->user->runtime_path, fifo_fd, s->seat ? s->seat->id : "", @@ -834,7 +834,7 @@ int session_send_create_reply(Session *s, sd_bus_error *error) { p, s->user->runtime_path, fifo_fd, - (uint32_t) s->user->uid, + (uint32_t) s->user->user_record->uid, s->seat ? s->seat->id : "", (uint32_t) s->vtnr, false); diff --git a/src/login/logind-session.c b/src/login/logind-session.c index a9be416f52..f9e122975e 100644 --- a/src/login/logind-session.c +++ b/src/login/logind-session.c @@ -230,8 +230,8 @@ int session_save(Session *s) { "IS_DISPLAY=%i\n" "STATE=%s\n" "REMOTE=%i\n", - s->user->uid, - s->user->name, + s->user->user_record->uid, + s->user->user_record->user_name, session_is_active(s), s->user->display == s, session_state_to_string(session_get_state(s)), @@ -641,7 +641,7 @@ static int session_start_scope(Session *s, sd_bus_message *properties, sd_bus_er if (!scope) return log_oom(); - description = strjoina("Session ", s->id, " of user ", s->user->name); + description = strjoina("Session ", s->id, " of user ", s->user->user_record->user_name); r = manager_start_scope( s->manager, @@ -657,7 +657,7 @@ static int session_start_scope(Session *s, sd_bus_message *properties, sd_bus_er "systemd-user-sessions.service", s->user->runtime_dir_service, s->user->service), - s->user->home, + user_record_home_directory(s->user->user_record), properties, error, &s->scope_job); @@ -698,9 +698,9 @@ int session_start(Session *s, sd_bus_message *properties, sd_bus_error *error) { log_struct(s->class == SESSION_BACKGROUND ? LOG_DEBUG : LOG_INFO, "MESSAGE_ID=" SD_MESSAGE_SESSION_START_STR, "SESSION_ID=%s", s->id, - "USER_ID=%s", s->user->name, + "USER_ID=%s", s->user->user_record->user_name, "LEADER="PID_FMT, s->leader, - LOG_MESSAGE("New session %s of user %s.", s->id, s->user->name)); + LOG_MESSAGE("New session %s of user %s.", s->id, s->user->user_record->user_name)); if (!dual_timestamp_is_set(&s->timestamp)) dual_timestamp_get(&s->timestamp); @@ -750,7 +750,7 @@ 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 || manager_shall_kill(s->manager, s->user->user_record->user_name)) { r = manager_stop_unit(s->manager, s->scope, &error, &s->scope_job); if (r < 0) { @@ -766,7 +766,7 @@ static int session_stop_scope(Session *s, bool force) { * Session stop is quite significant on its own, let's log it. */ log_struct(s->class == SESSION_BACKGROUND ? LOG_DEBUG : LOG_INFO, "SESSION_ID=%s", s->id, - "USER_ID=%s", s->user->name, + "USER_ID=%s", s->user->user_record->user_name, "LEADER="PID_FMT, s->leader, LOG_MESSAGE("Session %s logged out. Waiting for processes to exit.", s->id)); } @@ -824,7 +824,7 @@ int session_finalize(Session *s) { log_struct(s->class == SESSION_BACKGROUND ? LOG_DEBUG : LOG_INFO, "MESSAGE_ID=" SD_MESSAGE_SESSION_STOP_STR, "SESSION_ID=%s", s->id, - "USER_ID=%s", s->user->name, + "USER_ID=%s", s->user->user_record->user_name, "LEADER="PID_FMT, s->leader, LOG_MESSAGE("Removed session %s.", s->id)); @@ -1189,7 +1189,7 @@ static int session_prepare_vt(Session *s) { if (vt < 0) return vt; - r = fchown(vt, s->user->uid, -1); + r = fchown(vt, s->user->user_record->uid, -1); if (r < 0) { r = log_error_errno(errno, "Cannot change owner of /dev/tty%u: %m", diff --git a/src/login/logind-user-dbus.c b/src/login/logind-user-dbus.c index 1dc222f90e..7943a007e1 100644 --- a/src/login/logind-user-dbus.c +++ b/src/login/logind-user-dbus.c @@ -15,6 +15,60 @@ #include "strv.h" #include "user-util.h" +static int property_get_uid( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + User *u = userdata; + + assert(bus); + assert(reply); + assert(u); + + return sd_bus_message_append(reply, "u", (uint32_t) u->user_record->uid); +} + +static int property_get_gid( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + User *u = userdata; + + assert(bus); + assert(reply); + assert(u); + + return sd_bus_message_append(reply, "u", (uint32_t) u->user_record->gid); +} + +static int property_get_name( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + User *u = userdata; + + assert(bus); + assert(reply); + assert(u); + + return sd_bus_message_append(reply, "s", u->user_record->user_name); +} + static BUS_DEFINE_PROPERTY_GET2(property_get_state, "s", User, user_get_state, user_state_to_string); static int property_get_display( @@ -152,7 +206,7 @@ int bus_user_method_terminate(sd_bus_message *message, void *userdata, sd_bus_er "org.freedesktop.login1.manage", NULL, false, - u->uid, + u->user_record->uid, &u->manager->polkit_registry, error); if (r < 0) @@ -181,7 +235,7 @@ int bus_user_method_kill(sd_bus_message *message, void *userdata, sd_bus_error * "org.freedesktop.login1.manage", NULL, false, - u->uid, + u->user_record->uid, &u->manager->polkit_registry, error); if (r < 0) @@ -206,9 +260,9 @@ int bus_user_method_kill(sd_bus_message *message, void *userdata, sd_bus_error * const sd_bus_vtable user_vtable[] = { SD_BUS_VTABLE_START(0), - SD_BUS_PROPERTY("UID", "u", bus_property_get_uid, offsetof(User, uid), SD_BUS_VTABLE_PROPERTY_CONST), - SD_BUS_PROPERTY("GID", "u", bus_property_get_gid, offsetof(User, gid), SD_BUS_VTABLE_PROPERTY_CONST), - SD_BUS_PROPERTY("Name", "s", NULL, offsetof(User, name), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("UID", "u", property_get_uid, 0, SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("GID", "u", property_get_gid, 0, SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("Name", "s", property_get_name, 0, SD_BUS_VTABLE_PROPERTY_CONST), BUS_PROPERTY_DUAL_TIMESTAMP("Timestamp", offsetof(User, timestamp), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("RuntimePath", "s", NULL, offsetof(User, runtime_path), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("Service", "s", NULL, offsetof(User, service), SD_BUS_VTABLE_PROPERTY_CONST), @@ -276,7 +330,7 @@ char *user_bus_path(User *u) { assert(u); - if (asprintf(&s, "/org/freedesktop/login1/user/_"UID_FMT, u->uid) < 0) + if (asprintf(&s, "/org/freedesktop/login1/user/_"UID_FMT, u->user_record->uid) < 0) return NULL; return s; @@ -345,7 +399,7 @@ int user_send_signal(User *u, bool new_user) { "/org/freedesktop/login1", "org.freedesktop.login1.Manager", new_user ? "UserNew" : "UserRemoved", - "uo", (uint32_t) u->uid, p); + "uo", (uint32_t) u->user_record->uid, p); } int user_send_changed(User *u, const char *properties, ...) { diff --git a/src/login/logind-user.c b/src/login/logind-user.c index 1550431bea..f078f3f801 100644 --- a/src/login/logind-user.c +++ b/src/login/logind-user.c @@ -37,10 +37,7 @@ int user_new(User **ret, Manager *m, - uid_t uid, - gid_t gid, - const char *name, - const char *home) { + UserRecord *ur) { _cleanup_(user_freep) User *u = NULL; char lu[DECIMAL_STR_MAX(uid_t) + 1]; @@ -48,7 +45,13 @@ int user_new(User **ret, assert(ret); assert(m); - assert(name); + assert(ur); + + if (!ur->user_name) + return -EINVAL; + + if (!uid_is_valid(ur->uid)) + return -EINVAL; u = new(User, 1); if (!u) @@ -56,28 +59,17 @@ int user_new(User **ret, *u = (User) { .manager = m, - .uid = uid, - .gid = gid, + .user_record = user_record_ref(ur), .last_session_timestamp = USEC_INFINITY, }; - u->name = strdup(name); - if (!u->name) + if (asprintf(&u->state_file, "/run/systemd/users/" UID_FMT, ur->uid) < 0) return -ENOMEM; - u->home = strdup(home); - if (!u->home) + if (asprintf(&u->runtime_path, "/run/user/" UID_FMT, ur->uid) < 0) return -ENOMEM; - path_simplify(u->home, true); - - if (asprintf(&u->state_file, "/run/systemd/users/"UID_FMT, uid) < 0) - return -ENOMEM; - - if (asprintf(&u->runtime_path, "/run/user/"UID_FMT, uid) < 0) - return -ENOMEM; - - xsprintf(lu, UID_FMT, uid); + xsprintf(lu, UID_FMT, ur->uid); r = slice_build_subslice(SPECIAL_USER_SLICE, lu, &u->slice); if (r < 0) return r; @@ -90,7 +82,7 @@ int user_new(User **ret, if (r < 0) return r; - r = hashmap_put(m->users, UID_TO_PTR(uid), u); + r = hashmap_put(m->users, UID_TO_PTR(ur->uid), u); if (r < 0) return r; @@ -129,9 +121,9 @@ User *user_free(User *u) { if (u->slice) hashmap_remove_value(u->manager->user_units, u->slice, u); - hashmap_remove_value(u->manager->users, UID_TO_PTR(u->uid), u); + hashmap_remove_value(u->manager->users, UID_TO_PTR(u->user_record->uid), u); - (void) sd_event_source_unref(u->timer_event_source); + sd_event_source_unref(u->timer_event_source); u->service_job = mfree(u->service_job); @@ -140,8 +132,8 @@ User *user_free(User *u) { u->slice = mfree(u->slice); u->runtime_path = mfree(u->runtime_path); u->state_file = mfree(u->state_file); - u->name = mfree(u->name); - u->home = mfree(u->home); + + user_record_unref(u->user_record); return mfree(u); } @@ -169,7 +161,7 @@ static int user_save_internal(User *u) { "NAME=%s\n" "STATE=%s\n" /* friendly user-facing state */ "STOPPING=%s\n", /* low-level state */ - u->name, + u->user_record->user_name, user_state_to_string(user_get_state(u)), yes_no(u->stopping)); @@ -376,7 +368,7 @@ 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. */ @@ -460,7 +452,7 @@ int user_finalize(User *u) { * done. This is called as a result of an earlier user_done() when all jobs are completed. */ if (u->started) - log_debug("User %s logged out.", u->name); + log_debug("User %s logged out.", u->user_record->user_name); LIST_FOREACH(sessions_by_user, s, u->sessions) { k = session_finalize(s); @@ -474,8 +466,8 @@ int user_finalize(User *u) { * cases, as we shouldn't accidentally remove a system service's IPC objects while it is running, just because * a cronjob running as the same user just finished. Hence: exclude system users generally from IPC clean-up, * and do it only for normal users. */ - if (u->manager->remove_ipc && !uid_is_system(u->uid)) { - k = clean_ipc_by_uid(u->uid); + if (u->manager->remove_ipc && !uid_is_system(u->user_record->uid)) { + k = clean_ipc_by_uid(u->user_record->uid); if (k < 0) r = k; } @@ -531,7 +523,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; @@ -712,7 +704,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)) { @@ -775,7 +767,7 @@ 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, + u->user_record->user_name, format_timespan(s, sizeof(s), u->manager->user_stop_delay, USEC_PER_MSEC)); } } diff --git a/src/login/logind-user.h b/src/login/logind-user.h index 4bd65d8373..f8f172cb0f 100644 --- a/src/login/logind-user.h +++ b/src/login/logind-user.h @@ -6,6 +6,7 @@ typedef struct User User; #include "conf-parser.h" #include "list.h" #include "logind.h" +#include "user-record.h" typedef enum UserState { USER_OFFLINE, /* Not logged in at all */ @@ -20,10 +21,9 @@ typedef enum UserState { struct User { Manager *manager; - uid_t uid; - gid_t gid; - char *name; - char *home; + + UserRecord *user_record; + char *state_file; char *runtime_path; @@ -50,7 +50,7 @@ struct User { LIST_FIELDS(User, gc_queue); }; -int user_new(User **out, Manager *m, uid_t uid, gid_t gid, const char *name, const char *home); +int user_new(User **out, Manager *m, UserRecord *ur); User *user_free(User *u); DEFINE_TRIVIAL_CLEANUP_FUNC(User *, user_free); diff --git a/src/login/logind.h b/src/login/logind.h index f0f8928ef2..7e4673d27d 100644 --- a/src/login/logind.h +++ b/src/login/logind.h @@ -12,6 +12,7 @@ #include "list.h" #include "set.h" #include "time-util.h" +#include "user-record.h" typedef struct Manager Manager; @@ -28,7 +29,7 @@ struct Manager { Hashmap *seats; Hashmap *sessions; Hashmap *sessions_by_leader; - Hashmap *users; + Hashmap *users; /* indexed by UID */ Hashmap *inhibitors; Hashmap *buttons; Hashmap *brightness_writers; @@ -130,7 +131,7 @@ int manager_add_device(Manager *m, const char *sysfs, bool master, Device **ret_ int manager_add_button(Manager *m, const char *name, Button **ret_button); int manager_add_seat(Manager *m, const char *id, Seat **ret_seat); int manager_add_session(Manager *m, const char *id, Session **ret_session); -int manager_add_user(Manager *m, uid_t uid, gid_t gid, const char *name, const char *home, User **ret_user); +int manager_add_user(Manager *m, UserRecord *ur, User **ret_user); int manager_add_user_by_name(Manager *m, const char *name, User **ret_user); int manager_add_user_by_uid(Manager *m, uid_t uid, User **ret_user); int manager_add_inhibitor(Manager *m, const char* id, Inhibitor **ret_inhibitor); From e8e4b7a0b6ef1e181c5a288ccf2cab74d22ac28b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 8 Aug 2019 16:58:06 +0200 Subject: [PATCH 14/29] logind: enforce user record resource settings when user logs in --- src/login/logind-user.c | 97 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/src/login/logind-user.c b/src/login/logind-user.c index f078f3f801..6c2e445e6c 100644 --- a/src/login/logind-user.c +++ b/src/login/logind-user.c @@ -355,6 +355,100 @@ static void user_start_service(User *u) { "Failed to start user service '%s', ignoring: %s", u->service, bus_error_message(&error, r)); } +static int update_slice_callback(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { + _cleanup_(user_record_unrefp) UserRecord *ur = userdata; + + assert(m); + assert(ur); + + if (sd_bus_message_is_method_error(m, NULL)) { + log_warning_errno(sd_bus_message_get_errno(m), + "Failed to update slice of %s, ignoring: %s", + ur->user_name, + sd_bus_message_get_error(m)->message); + + return 0; + } + + log_debug("Successfully set slice parameters of %s.", ur->user_name); + return 0; +} + +static int user_update_slice(User *u) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + int r; + + assert(u); + + if (u->user_record->tasks_max == UINT64_MAX && + u->user_record->memory_high == UINT64_MAX && + u->user_record->memory_max == UINT64_MAX && + u->user_record->cpu_weight == UINT64_MAX && + u->user_record->io_weight == UINT64_MAX) + return 0; + + r = sd_bus_message_new_method_call( + u->manager->bus, + &m, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + "SetUnitProperties"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "sb", u->slice, true); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_open_container(m, 'a', "(sv)"); + if (r < 0) + return bus_log_create_error(r); + + if (u->user_record->tasks_max != UINT64_MAX) { + r = sd_bus_message_append(m, "(sv)", "TasksMax", "t", u->user_record->tasks_max); + if (r < 0) + return bus_log_create_error(r); + } + + if (u->user_record->memory_max != UINT64_MAX) { + r = sd_bus_message_append(m, "(sv)", "MemoryMax", "t", u->user_record->memory_max); + if (r < 0) + return bus_log_create_error(r); + } + + if (u->user_record->memory_high != UINT64_MAX) { + r = sd_bus_message_append(m, "(sv)", "MemoryHigh", "t", u->user_record->memory_high); + if (r < 0) + return bus_log_create_error(r); + } + + if (u->user_record->cpu_weight != UINT64_MAX) { + r = sd_bus_message_append(m, "(sv)", "CPUWeight", "t", u->user_record->cpu_weight); + if (r < 0) + return bus_log_create_error(r); + } + + if (u->user_record->io_weight != UINT64_MAX) { + r = sd_bus_message_append(m, "(sv)", "IOWeight", "t", u->user_record->io_weight); + if (r < 0) + return bus_log_create_error(r); + } + + r = sd_bus_message_close_container(m); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call_async(u->manager->bus, NULL, m, update_slice_callback, u->user_record, 0); + if (r < 0) + return log_error_errno(r, "Failed to change user slice properties: %m"); + + /* Ref the user record pointer, so that the slot keeps it pinned */ + user_record_ref(u->user_record); + + return 0; +} + int user_start(User *u) { assert(u); @@ -374,6 +468,9 @@ int user_start(User *u) { * 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); From 156a363750b36686b3ee948e769ec99276f58ee4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 9 Aug 2019 13:46:25 +0200 Subject: [PATCH 15/29] logind: honour killProcesses field of user record --- src/login/logind-session.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/login/logind-session.c b/src/login/logind-session.c index f9e122975e..48505122a9 100644 --- a/src/login/logind-session.c +++ b/src/login/logind-session.c @@ -750,7 +750,10 @@ static int session_stop_scope(Session *s, bool force) { s->scope_job = mfree(s->scope_job); /* Optionally, let's kill everything that's left now. */ - if (force || manager_shall_kill(s->manager, s->user->user_record->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) { From d510589fd0a491a55db46a462271b53fe534c00e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 9 Aug 2019 13:46:43 +0200 Subject: [PATCH 16/29] logind: honour per-user stopDelayUSec property --- src/login/logind-user.c | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/login/logind-user.c b/src/login/logind-user.c index 6c2e445e6c..fdc6448a65 100644 --- a/src/login/logind-user.c +++ b/src/login/logind-user.c @@ -656,6 +656,18 @@ static bool user_unit_active(User *u) { return false; } +static usec_t user_get_stop_delay(User *u) { + assert(u); + + if (u->user_record->stop_delay_usec != UINT64_MAX) + return u->user_record->stop_delay_usec; + + if (user_record_removable(u->user_record) > 0) + return 0; /* For removable users lower the stop delay to zero */ + + return u->manager->user_stop_delay; +} + bool user_may_gc(User *u, bool drop_not_started) { int r; @@ -668,12 +680,16 @@ bool user_may_gc(User *u, bool drop_not_started) { return false; if (u->last_session_timestamp != USEC_INFINITY) { + usec_t user_stop_delay; + /* All sessions have been closed. Let's see if we shall leave the user record around for a bit */ - if (u->manager->user_stop_delay == USEC_INFINITY) + user_stop_delay = user_get_stop_delay(u); + + if (user_stop_delay == USEC_INFINITY) return false; /* Leave it around forever! */ - if (u->manager->user_stop_delay > 0 && - now(CLOCK_MONOTONIC) < usec_add(u->last_session_timestamp, u->manager->user_stop_delay)) + if (user_stop_delay > 0 && + now(CLOCK_MONOTONIC) < usec_add(u->last_session_timestamp, user_stop_delay)) return false; /* Leave it around for a bit longer. */ } @@ -826,6 +842,7 @@ static int user_stop_timeout_callback(sd_event_source *es, uint64_t usec, void * } void user_update_last_session_timer(User *u) { + usec_t user_stop_delay; int r; assert(u); @@ -844,7 +861,8 @@ void user_update_last_session_timer(User *u) { assert(!u->timer_event_source); - if (IN_SET(u->manager->user_stop_delay, 0, USEC_INFINITY)) + user_stop_delay = user_get_stop_delay(u); + if (IN_SET(user_stop_delay, 0, USEC_INFINITY)) return; if (sd_event_get_state(u->manager->event) == SD_EVENT_FINISHED) { @@ -855,7 +873,7 @@ void user_update_last_session_timer(User *u) { r = sd_event_add_time(u->manager->event, &u->timer_event_source, CLOCK_MONOTONIC, - usec_add(u->last_session_timestamp, u->manager->user_stop_delay), 0, + usec_add(u->last_session_timestamp, user_stop_delay), 0, user_stop_timeout_callback, u); if (r < 0) log_warning_errno(r, "Failed to enqueue user stop event source, ignoring: %m"); @@ -865,7 +883,7 @@ void user_update_last_session_timer(User *u) { log_debug("Last session of user '%s' logged out, terminating user context in %s.", u->user_record->user_name, - format_timespan(s, sizeof(s), u->manager->user_stop_delay, USEC_PER_MSEC)); + format_timespan(s, sizeof(s), user_stop_delay, USEC_PER_MSEC)); } } From cef9f2a64766a3d082720599eb80db66302dbce9 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 18 Dec 2019 10:06:34 +0100 Subject: [PATCH 17/29] shared: add pam utility helpers --- src/shared/meson.build | 8 +++++ src/shared/pam-util.c | 81 ++++++++++++++++++++++++++++++++++++++++++ src/shared/pam-util.h | 15 ++++++++ 3 files changed, 104 insertions(+) create mode 100644 src/shared/pam-util.c create mode 100644 src/shared/pam-util.h diff --git a/src/shared/meson.build b/src/shared/meson.build index 4d98fb2c17..a7320fc4ed 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -249,6 +249,13 @@ if conf.get('HAVE_KMOD') == 1 shared_sources += files('module-util.c') endif +if conf.get('HAVE_PAM') == 1 + shared_sources += files(''' + pam-util.c + pam-util.h +'''.split()) +endif + generate_ip_protocol_list = find_program('generate-ip-protocol-list.sh') ip_protocol_list_txt = custom_target( 'ip-protocol-list.txt', @@ -305,6 +312,7 @@ libshared_deps = [threads, libmount, libopenssl, libp11kit, + libpam, librt, libseccomp, libselinux, diff --git a/src/shared/pam-util.c b/src/shared/pam-util.c new file mode 100644 index 0000000000..f000798ce0 --- /dev/null +++ b/src/shared/pam-util.c @@ -0,0 +1,81 @@ +#include +#include +#include + +#include "alloc-util.h" +#include "errno-util.h" +#include "macro.h" +#include "pam-util.h" + +int pam_log_oom(pam_handle_t *handle) { + /* This is like log_oom(), but uses PAM logging */ + pam_syslog(handle, LOG_ERR, "Out of memory."); + return PAM_BUF_ERR; +} + +int pam_bus_log_create_error(pam_handle_t *handle, int r) { + /* This is like bus_log_create_error(), but uses PAM logging */ + pam_syslog(handle, LOG_ERR, "Failed to create bus message: %s", strerror_safe(r)); + return PAM_BUF_ERR; +} + +int pam_bus_log_parse_error(pam_handle_t *handle, int r) { + /* This is like bus_log_parse_error(), but uses PAM logging */ + pam_syslog(handle, LOG_ERR, "Failed to parse bus message: %s", strerror_safe(r)); + return PAM_BUF_ERR; +} + +static void cleanup_system_bus(pam_handle_t *handle, void *data, int error_status) { + sd_bus_flush_close_unref(data); +} + +int pam_acquire_bus_connection(pam_handle_t *handle, sd_bus **ret) { + _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL; + int r; + + assert(handle); + assert(ret); + + /* We cache the bus connection so that we can share it between the session and the authentication hooks */ + r = pam_get_data(handle, "systemd-system-bus", (const void**) &bus); + if (r == PAM_SUCCESS && bus) { + *ret = sd_bus_ref(TAKE_PTR(bus)); /* Increase the reference counter, so that the PAM data stays valid */ + return PAM_SUCCESS; + } + if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) { + pam_syslog(handle, LOG_ERR, "Failed to get bus connection: %s", pam_strerror(handle, r)); + return r; + } + + r = sd_bus_open_system(&bus); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to connect to system bus: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + r = pam_set_data(handle, "systemd-system-bus", bus, cleanup_system_bus); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM bus data: %s", pam_strerror(handle, r)); + return r; + } + + sd_bus_ref(bus); + *ret = TAKE_PTR(bus); + + return PAM_SUCCESS; +} + +int pam_release_bus_connection(pam_handle_t *handle) { + int r; + + r = pam_set_data(handle, "systemd-system-bus", NULL, NULL); + if (r != PAM_SUCCESS) + pam_syslog(handle, LOG_ERR, "Failed to release PAM user record data: %s", pam_strerror(handle, r)); + + return r; +} + +void pam_cleanup_free(pam_handle_t *handle, void *data, int error_status) { + /* A generic destructor for pam_set_data() that just frees the specified data */ + free(data); +} diff --git a/src/shared/pam-util.h b/src/shared/pam-util.h new file mode 100644 index 0000000000..26d07b7f0c --- /dev/null +++ b/src/shared/pam-util.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include + +#include "sd-bus.h" + +int pam_log_oom(pam_handle_t *handle); +int pam_bus_log_create_error(pam_handle_t *handle, int r); +int pam_bus_log_parse_error(pam_handle_t *handle, int r); + +int pam_acquire_bus_connection(pam_handle_t *handle, sd_bus **ret); +int pam_release_bus_connection(pam_handle_t *handle); + +void pam_cleanup_free(pam_handle_t *handle, void *data, int error_status); From d750dde2a634e9a5354adf7d4798d16d9129ce28 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 12 Aug 2019 16:39:10 +0200 Subject: [PATCH 18/29] pam-systemd: port to pam_bus_log_{create|parse}_error() and pam_log_oom() --- src/login/pam_systemd.c | 107 +++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 61 deletions(-) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index aa6e5ea7ac..0647c8b90d 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -28,6 +28,7 @@ #include "hostname-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" @@ -233,17 +234,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) { @@ -255,31 +254,31 @@ static int append_session_memory_max(pam_handle_t *handle, sd_bus_message *m, co 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 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 0; + } + + 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 0; + } + + pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.memory_max, ignoring: %s", limit); return 0; } @@ -315,12 +314,10 @@ static int append_session_tasks_max(pam_handle_t *handle, sd_bus_message *m, con 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; } @@ -335,14 +332,12 @@ static int append_session_cg_weight(pam_handle_t *handle, sd_bus_message *m, con 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; } @@ -599,10 +594,8 @@ _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, @@ -618,16 +611,12 @@ _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) @@ -650,10 +639,8 @@ _public_ PAM_EXTERN int pam_sm_open_session( return PAM_SESSION_ERR; r = sd_bus_message_close_container(m); - if (r < 0) { - pam_syslog(handle, LOG_ERR, "Failed to close message container: %s", strerror_safe(r)); - return PAM_SYSTEM_ERR; - } + if (r < 0) + return pam_bus_log_create_error(handle, r); r = sd_bus_call(bus, m, LOGIN_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply); if (r < 0) { @@ -677,10 +664,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: " From 355c9966c207cb1e86791f518accb16761769639 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 12 Aug 2019 16:55:48 +0200 Subject: [PATCH 19/29] pam-systemd: share bus connection with pam_systemd_home if we can Let's use the pam-util.h provided helpers to acquire them. --- src/login/pam_systemd.c | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 0647c8b90d..3c283c74f5 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -568,11 +568,9 @@ _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: " @@ -744,6 +742,10 @@ _public_ PAM_EXTERN int pam_sm_open_session( } } + /* 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; } @@ -752,8 +754,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; @@ -766,17 +766,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", @@ -793,11 +791,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; } From 9ab0d3ebe5a57fa863cc7e80d2a995d9950eeae8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 12 Aug 2019 18:55:26 +0200 Subject: [PATCH 20/29] pam-systemd: port over to use a UserRecord structure Later on this allows us to set various session properties from user record. --- src/login/pam_systemd.c | 107 ++++++++++++++++++++++++++++++---------- 1 file changed, 82 insertions(+), 25 deletions(-) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 3c283c74f5..478b71f036 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -36,6 +36,8 @@ #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) @@ -87,18 +89,15 @@ static int parse_argv( return 0; } -static int get_user_data( +static int acquire_user_record( pam_handle_t *handle, - const char **ret_username, - struct passwd **ret_pw) { + UserRecord **ret_record) { - const char *username = NULL; - struct passwd *pw = NULL; + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; + const char *username = NULL, *json = NULL; int r; assert(handle); - assert(ret_username); - assert(ret_pw); r = pam_get_user(handle, &username, NULL); if (r != PAM_SUCCESS) { @@ -108,17 +107,75 @@ static int get_user_data( if (isempty(username)) { pam_syslog(handle, LOG_ERR, "User name not valid."); - return PAM_AUTH_ERR; + return PAM_SERVICE_ERR; } - pw = pam_modutil_getpwnam(handle, username); - if (!pw) { - pam_syslog(handle, LOG_ERR, "Failed to get user data."); - return PAM_USER_UNKNOWN; + /* If pam_systemd_homed (or some other module) already acqired the user record we can reuse it + * here. */ + r = pam_get_data(handle, "systemd-user-record", (const void**) &json); + if (r != PAM_SUCCESS || !json) { + _cleanup_free_ char *formatted = NULL; + + if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) { + pam_syslog(handle, LOG_ERR, "Failed to get PAM user record data: %s", pam_strerror(handle, r)); + return r; + } + + /* Request the record ourselves */ + r = userdb_by_name(username, 0, &ur); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to get user record: %s", strerror_safe(r)); + return PAM_USER_UNKNOWN; + } + + r = json_variant_format(ur->json, 0, &formatted); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to format user JSON: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + /* And cache it for everyone else */ + r = pam_set_data(handle, "systemd-user-record", formatted, pam_cleanup_free); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM user record data: %s", pam_strerror(handle, r)); + return r; + } + + TAKE_PTR(formatted); + } else { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + + /* Parse cached record */ + r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to parse JSON user record: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + ur = user_record_new(); + if (!ur) + return pam_log_oom(handle); + + r = user_record_load(ur, v, USER_RECORD_LOAD_REFUSE_SECRET); + if (r < 0) { + pam_syslog(handle, LOG_ERR, "Failed to load user record: %s", strerror_safe(r)); + return PAM_SERVICE_ERR; + } + + /* Safety check if cached record actually matches what we are looking for */ + if (!streq_ptr(username, ur->user_name)) { + pam_syslog(handle, LOG_ERR, "Acquired user record does not match user name."); + return PAM_SERVICE_ERR; + } } - *ret_pw = pw; - *ret_username = username; + if (!uid_is_valid(ur->uid)) { + pam_syslog(handle, LOG_ERR, "Acquired user record does not have a UID."); + return PAM_SERVICE_ERR; + } + + if (ret_record) + *ret_record = TAKE_PTR(ur); return PAM_SUCCESS; } @@ -426,7 +483,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, @@ -435,9 +492,9 @@ _public_ PAM_EXTERN int pam_sm_open_session( *class_pam = NULL, *type_pam = NULL, *cvtnr = NULL, *desktop = NULL, *desktop_pam = NULL, *memory_max = NULL, *tasks_max = NULL, *cpu_weight = NULL, *io_weight = NULL, *runtime_max_sec = NULL; _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_(user_record_unrefp) UserRecord *ur = NULL; int session_fd = -1, existing, r; bool debug = false, remote; - struct passwd *pw; uint32_t vtnr = 0; uid_t original_uid; @@ -458,7 +515,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( if (debug) pam_syslog(handle, LOG_DEBUG, "pam-systemd initializing"); - r = get_user_data(handle, &username, &pw); + r = acquire_user_record(handle, &ur); if (r != PAM_SUCCESS) return r; @@ -472,8 +529,8 @@ _public_ PAM_EXTERN int pam_sm_open_session( if (streq_ptr(service, "systemd-user")) { char rt[STRLEN("/run/user/") + DECIMAL_STR_MAX(uid_t)]; - xsprintf(rt, "/run/user/"UID_FMT, pw->pw_uid); - if (validate_runtime_directory(handle, rt, pw->pw_uid)) { + xsprintf(rt, "/run/user/"UID_FMT, ur->uid); + if (validate_runtime_directory(handle, rt, ur->uid)) { r = pam_misc_setenv(handle, "XDG_RUNTIME_DIR", rt, 0); if (r != PAM_SUCCESS) { pam_syslog(handle, LOG_ERR, "Failed to set runtime dir: %s", pam_strerror(handle, r)); @@ -481,7 +538,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( } } - r = export_legacy_dbus_address(handle, pw->pw_uid, rt); + r = export_legacy_dbus_address(handle, ur->uid, rt); if (r != PAM_SUCCESS) return r; @@ -575,7 +632,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( 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), @@ -596,7 +653,7 @@ _public_ PAM_EXTERN int pam_sm_open_session( 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, @@ -674,20 +731,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; } From 7bfbf6cc92bdaecfe21b0a9b7f00468a189c88e4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 13 Aug 2019 13:17:51 +0200 Subject: [PATCH 21/29] pam-systemd: normalize return values of append_session_xyz() Let's propagate the PAM errors we got. --- src/login/pam_systemd.c | 48 ++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 478b71f036..6aded5b146 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -307,14 +307,14 @@ 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) return pam_bus_log_create_error(handle, r); - return 0; + return PAM_SUCCESS; } r = parse_permille(limit); @@ -323,7 +323,7 @@ static int append_session_memory_max(pam_handle_t *handle, sd_bus_message *m, co if (r < 0) return pam_bus_log_create_error(handle, r); - return 0; + return PAM_SUCCESS; } r = parse_size(limit, 1024, &val); @@ -332,11 +332,11 @@ static int append_session_memory_max(pam_handle_t *handle, sd_bus_message *m, co if (r < 0) return pam_bus_log_create_error(handle, r); - return 0; + return PAM_SUCCESS; } pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.memory_max, ignoring: %s", limit); - return 0; + return PAM_SUCCESS; } static int append_session_runtime_max_sec(pam_handle_t *handle, sd_bus_message *m, const char *limit) { @@ -345,19 +345,17 @@ static int append_session_runtime_max_sec(pam_handle_t *handle, sd_bus_message * /* No need to parse "infinity" here, it will be set by default later in scope_init() */ if (isempty(limit) || streq(limit, "infinity")) - return 0; + return PAM_SUCCESS; r = parse_sec(limit, &val); if (r >= 0) { r = sd_bus_message_append(m, "(sv)", "RuntimeMaxUSec", "t", (uint64_t) val); - if (r < 0) { - pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r)); - return r; - } + if (r < 0) + return pam_bus_log_create_error(handle, r); } else pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.runtime_max_sec: %s, ignoring.", limit); - return 0; + return PAM_SUCCESS; } static int append_session_tasks_max(pam_handle_t *handle, sd_bus_message *m, const char *limit) { @@ -366,7 +364,7 @@ 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) { @@ -376,7 +374,7 @@ static int append_session_tasks_max(pam_handle_t *handle, sd_bus_message *m, con } else 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) { @@ -384,7 +382,7 @@ 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) { @@ -396,7 +394,7 @@ static int append_session_cg_weight(pam_handle_t *handle, sd_bus_message *m, con else 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) { @@ -674,24 +672,24 @@ _public_ PAM_EXTERN int pam_sm_open_session( return pam_bus_log_create_error(handle, r); r = append_session_memory_max(handle, m, memory_max); - if (r < 0) - return PAM_SESSION_ERR; + if (r != PAM_SUCCESS) + return r; r = append_session_runtime_max_sec(handle, m, runtime_max_sec); - if (r < 0) - return PAM_SESSION_ERR; + if (r != PAM_SUCCESS) + return r; r = append_session_tasks_max(handle, m, tasks_max); - if (r < 0) - return PAM_SESSION_ERR; + if (r != PAM_SUCCESS) + return r; r = append_session_cg_weight(handle, m, cpu_weight, "CPUWeight"); - if (r < 0) - return PAM_SESSION_ERR; + if (r != PAM_SUCCESS) + return r; r = append_session_cg_weight(handle, m, io_weight, "IOWeight"); - if (r < 0) - return PAM_SESSION_ERR; + if (r != PAM_SUCCESS) + return r; r = sd_bus_message_close_container(m); if (r < 0) From f9c1f4e19308c00a4f5b76fdf18549a75fa418ff Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 13 Aug 2019 13:18:15 +0200 Subject: [PATCH 22/29] pam-systemd: apply user record properties to session This way any component providing us with JSON user record data can use this for automatic resource management and other session properties. --- src/login/pam_systemd.c | 147 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 145 insertions(+), 2 deletions(-) diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 6aded5b146..c7fe858b92 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -25,13 +25,16 @@ #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" @@ -473,6 +476,138 @@ fail: return false; } +static int pam_putenv_and_log(pam_handle_t *handle, const char *e, bool debug) { + int r; + + assert(handle); + assert(e); + + r = pam_putenv(handle, e); + if (r != PAM_SUCCESS) { + pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable %s: %s", e, pam_strerror(handle, r)); + return r; + } + + if (debug) + pam_syslog(handle, LOG_DEBUG, "PAM environment variable %s set based on user record.", e); + + return PAM_SUCCESS; +} + +static int apply_user_record_settings(pam_handle_t *handle, UserRecord *ur, bool debug) { + char **i; + int r; + + assert(handle); + assert(ur); + + if (ur->umask != MODE_INVALID) { + umask(ur->umask); + + if (debug) + pam_syslog(handle, LOG_DEBUG, "Set user umask to %04o based on user record.", ur->umask); + } + + STRV_FOREACH(i, ur->environment) { + _cleanup_free_ char *n = NULL; + const char *e; + + assert_se(e = strchr(*i, '=')); /* environment was already validated while parsing JSON record, this thus must hold */ + + n = strndup(*i, e - *i); + if (!n) + return pam_log_oom(handle); + + if (pam_getenv(handle, n)) { + if (debug) + pam_syslog(handle, LOG_DEBUG, "PAM environment variable $%s already set, not changing based on record.", *i); + continue; + } + + r = pam_putenv_and_log(handle, *i, debug); + if (r != PAM_SUCCESS) + return r; + } + + if (ur->email_address) { + if (pam_getenv(handle, "EMAIL")) { + if (debug) + pam_syslog(handle, LOG_DEBUG, "PAM environment variable $EMAIL already set, not changing based on user record."); + } else { + _cleanup_free_ char *joined = NULL; + + joined = strjoin("EMAIL=", ur->email_address); + if (!joined) + return pam_log_oom(handle); + + r = pam_putenv_and_log(handle, joined, debug); + if (r != PAM_SUCCESS) + return r; + } + } + + if (ur->time_zone) { + if (pam_getenv(handle, "TZ")) { + if (debug) + pam_syslog(handle, LOG_DEBUG, "PAM environment variable $TZ already set, not changing based on user record."); + } else if (!timezone_is_valid(ur->time_zone, LOG_DEBUG)) { + if (debug) + pam_syslog(handle, LOG_DEBUG, "Time zone specified in user record is not valid locally, not setting $TZ."); + } else { + _cleanup_free_ char *joined = NULL; + + joined = strjoin("TZ=:", ur->time_zone); + if (!joined) + return pam_log_oom(handle); + + r = pam_putenv_and_log(handle, joined, debug); + if (r != PAM_SUCCESS) + return r; + } + } + + if (ur->preferred_language) { + if (pam_getenv(handle, "LANG")) { + if (debug) + pam_syslog(handle, LOG_DEBUG, "PAM environment variable $LANG already set, not changing based on user record."); + } else if (!locale_is_valid(ur->preferred_language)) { + if (debug) + pam_syslog(handle, LOG_DEBUG, "Preferred language specified in user record is not valid locally, not setting $LANG."); + } else { + _cleanup_free_ char *joined = NULL; + + joined = strjoin("LANG=", ur->preferred_language); + if (!joined) + return pam_log_oom(handle); + + r = pam_putenv_and_log(handle, joined, debug); + if (r != PAM_SUCCESS) + return r; + } + } + + if (nice_is_valid(ur->nice_level)) { + if (nice(ur->nice_level) < 0) + pam_syslog(handle, LOG_ERR, "Failed to set nice level to %i, ignoring: %s", ur->nice_level, strerror_safe(errno)); + else if (debug) + pam_syslog(handle, LOG_DEBUG, "Nice level set, based on user record."); + } + + for (int rl = 0; rl < _RLIMIT_MAX; rl++) { + + if (!ur->rlimits[rl]) + continue; + + r = setrlimit_closest(rl, ur->rlimits[rl]); + if (r < 0) + pam_syslog(handle, LOG_ERR, "Failed to set resource limit %s, ignoring: %s", rlimit_to_string(rl), strerror_safe(r)); + else if (debug) + pam_syslog(handle, LOG_DEBUG, "Resource limit %s set, based on user record.", rlimit_to_string(rl)); + } + + return PAM_SUCCESS; +} + _public_ PAM_EXTERN int pam_sm_open_session( pam_handle_t *handle, int flags, @@ -540,6 +675,10 @@ _public_ PAM_EXTERN int pam_sm_open_session( if (r != PAM_SUCCESS) return r; + r = apply_user_record_settings(handle, ur, debug); + if (r != PAM_SUCCESS) + return r; + return PAM_SUCCESS; } @@ -797,9 +936,13 @@ _public_ PAM_EXTERN int pam_sm_open_session( } } + r = apply_user_record_settings(handle, ur, debug); + if (r != PAM_SUCCESS) + return r; + /* Let's release the D-Bus connection, after all the session might live quite a long time, and we are - * not going to process the bus connection in that time, so let's better close before the daemon - * kicks us off because we are not processing anything. */ + * not going to use the bus connection in that time, so let's better close before the daemon kicks us + * off because we are not processing anything. */ (void) pam_release_bus_connection(handle); return PAM_SUCCESS; } From fc89f88e56cd13a0caec4fa2adfecf9ae9c04c0c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 19 Nov 2019 11:30:41 +0100 Subject: [PATCH 23/29] man: document new pam_systemd features in man page This also updates the suggested PAM snippet in a number of way: 1. Be closer to the logic nowadays implemented in Fedora where the auth/account/password stacks are all finished off with pam_{deny|permit}.so 2. Make pam_unix.so just "sufficient" instead of "required" (paving ground for pam_systemd_home.so being hooked in as additional sufficient module. 3. Only do pam_nologin in the "account" stack, since it's about account validity really. 4. Use modern parameters to pam_unix when changing passwords, i.e. sha512 and shadow, and use already set up passwords (preparing ground for pam_systemd_home again) --- man/pam_systemd.xml | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/man/pam_systemd.xml b/man/pam_systemd.xml index 470c1a4c42..694371c2c4 100644 --- a/man/pam_systemd.xml +++ b/man/pam_systemd.xml @@ -32,6 +32,10 @@ systemd-logind.service8, and hence the systemd control group hierarchy. + The module also applies various resource management and runtime parameters to the new session, as + configured in the JSON User Record of the user, when + one is defined. + On login, this module — in conjunction with systemd-logind.service — ensures the following: @@ -48,7 +52,12 @@ A new systemd scope unit is created for the session. If this is the first concurrent session of the user, an implicit per-user slice unit below user.slice is automatically created and the scope placed into it. An instance of the system service user@.service, which runs the - systemd user manager instance, is started. + systemd user manager instance, is started. + + The $TZ, $EMAIL and $LANG + environment variables are configured for the user, based on the respective data from the user's JSON + record (if it is defined). Moreover, any environment variables explicitly configured in the user record + are imported, and the umask, nice level, and resource limits initialized. On logout, this module ensures the following: @@ -172,6 +181,15 @@ is not set if the current user is not the original user of the session. + + $TZ + $EMAIL + $LANG + + If a JSON user record is known for the user logging in these variables are + initialized from the respective data in the record. + + The following environment variables are read by the module and may be used by the PAM service to pass @@ -286,14 +304,23 @@ pam_set_data(handle, "systemd.runtime_max_sec", (void *)"3600", cleanup); Example + Here's an example PAM configuration fragment that allows users sessions to be managed by + systemd-logind.service: + #%PAM-1.0 -auth required pam_unix.so -auth required pam_nologin.so -account required pam_unix.so -password required pam_unix.so -session required pam_unix.so -session required pam_loginuid.so -session required pam_systemd.so +auth sufficient pam_unix.so +auth required pam_deny.so + +account required pam_nologin.so +account sufficient pam_unix.so +account required pam_permit.so + +password sufficient pam_unix.so sha512 shadow try_first_pass try_authtok +password required pam_deny.so + +-session optional pam_loginuid.so +-session optional pam_systemd.so +session required pam_unix.so @@ -303,6 +330,7 @@ session required pam_systemd.so systemd-logind.service8, logind.conf5, loginctl1, + pam_systemd_home8, pam.conf5, pam.d5, pam8, From 3b2db6f110f3de49e19696c10f116ac82909721e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 28 Aug 2019 12:40:23 +0200 Subject: [PATCH 24/29] man: document userdbctl(1) --- man/rules/meson.build | 1 + man/userdbctl.xml | 258 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 259 insertions(+) create mode 100644 man/userdbctl.xml diff --git a/man/rules/meson.build b/man/rules/meson.build index f33621a654..df965b0824 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -952,6 +952,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/userdbctl.xml b/man/userdbctl.xml new file mode 100644 index 0000000000..69cd3fc067 --- /dev/null +++ b/man/userdbctl.xml @@ -0,0 +1,258 @@ + + + + + + + + userdbctl + systemd + + + + userdbctl + 1 + + + + userdbctl + Inspect users, groups and group memberships + + + + + userdbctl + OPTIONS + COMMAND + NAME + + + + + Description + + userdbctl may be used to inspect user and groups (as well as group memberships) + of the system. This client utility inquires user/group information provided by various system services, + both operating on JSON user/group records (as defined by the JSON User Record and JSON Group Record definitions), and classic UNIX NSS/glibc + user and group records. This tool is primarily a client to the User/Group Record Lookup API via Varlink. + + + + Options + + The following options are understood: + + + + + MODE + + Choose the output mode, takes one of classic, + friendly, table, json. If + classic, an output very close to the format of /etc/passwd or + /etc/group is generated. If friendly a more comprehensive and + user friendly, human readable output is generated; if table a minimal, tabular + output is generated; if json a JSON formatted output is generated. Defaults to + friendly if a user/group is specified on the command line, + table otherwise. + + + + SERVICE:SERVICE… + SERVICE:SERVICE… + + Controls which services to query for users/groups. Takes a list of one or more + service names, separated by :. See below for a list of well-known service + names. If not specified all available services are queried at once. + + + + BOOL + + Controls whether to include classic glibc/NSS user/group lookups in the output. If + is used any attempts to resolve or enumerate users/groups provided + only via glibc NSS is suppressed. If is specified such users/groups + are included in the output (which is the default). + + + + BOOL + + Controls whether to synthesize records for the root and nobody users/groups if they + aren't defined otherwise. By default (or yes) such records are implicitly + synthesized if otherwise missing since they have special significance to the OS. When + no this synthesizing is turned off. + + + + + + This option is short for + . Use this option to show only records that are natively defined as + JSON user or group records, with all NSS/glibc compatibility and all implicit synthesis turned + off. + + + + + + + + + + + Commands + + The following commands are understood: + + + + + user USER… + + List all known users records or show details of one or more specified user + records. Use to tweak output mode. + + + + group GROUP… + + List all known group records or show details of one or more specified group + records. Use to tweak output mode. + + + + users-in-group GROUP… + + List users that are members of the specified groups. If no groups are specified list + all user/group memberships defined. Use to tweak output + mode. + + + + groups-of-user USER… + + List groups that the specified users are members of. If no users are specified list + all user/group memberships defined (in this case groups-of-user and + users-in-group are equivalent). Use to tweak output + mode. + + + + services + + List all services currently providing user/group definitions to the system. See below + for a list of well-known services providing user information. + + + + ssh-authorized-keys + + This operation is not a public, user-facing interface. It is used to allow the SSH daemon to pick + up authorized keys from user records, see below. + + + + + + Well-Known Services + + The userdbctl services command will list all currently running services that + provide user or group definitions to the system. The following are well-known services are shown among + this list. + + + + + io.systemd.DynamicUser + + This service is provided by the system service manager itself (i.e. PID 1) and + makes all users (and their groups) synthesized through the DynamicUser= setting in + service unit files available to the system (see + systemd.exec5 for + details about this setting). + + + + io.systemd.Home + + This service is provided by + systemd-homed.service8 + and makes all users (and their groups) belonging to home directories managed by that service + available to the system. + + + + io.systemd.Multiplexer + + This service is provided by + systemd-userdbd.service8 + and multiplexes user/group look-ups to all other running lookup services. This is the primary entry point + for user/group record clients, as it simplifies client side implementation substantially since they + can ask a single service for lookups instead of asking all running services in parallel. + userdbctl uses this service preferably, too, unless + or are used, in which case finer control over the services to talk to is + required. + + + + io.systemd.NameSeviceSwitch + + This service is (also) provided by + systemd-userdbd.service8 + and converts classic NSS/glibc user and group records to JSON user/group records, providing full + backwards compatibility. Use to disable this compatibility, see + above. Note that compatibility is actually provided in both directions: + nss-systemd8 will + automatically synthesize classic NSS/glibc user/group records from all JSON user/group records + provided to the system, thus using both APIs is mostly equivalent and provides access to the same + data, however the NSS/glibc APIs necessarily expose a more reduced set of fields + only. + + + + Note that userdbctl has internal support for NSS-based lookups too. This means + that if neither io.systemd.Multiplexer nor + io.systemd.NameSeviceSwitch are running look-ups into the the basic user/group + databases will still work. + + + + Integration with SSH + + The userdbctl tool may be used to make the list of SSH authorized keys possibly + contained in a user record available to the SSH daemon for authentication. For that configure the + following in sshd_config5: + + … +AuthorizedKeysCommand /usr/bin/userdbctl ssh-authorized-keys %u +AuthorizedKeysCommandUser root +… + + + + Exit status + + On success, 0 is returned, a non-zero failure code otherwise. + + + + + + See Also + + systemd1, + systemd-userdbd.service8, + systemd-homed.service8, + nss-systemd8, + getent1 + + + + From 7d9ad0e5e51c808e7cb5fcb2f251d9d912be4061 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 28 Aug 2019 13:33:13 +0200 Subject: [PATCH 25/29] man: document systemd-userdbd.service --- man/rules/meson.build | 1 + man/systemd-userdbd.service.xml | 69 +++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 man/systemd-userdbd.service.xml diff --git a/man/rules/meson.build b/man/rules/meson.build index df965b0824..90376da775 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -821,6 +821,7 @@ manpages = [ ['systemd-update-utmp', 'systemd-update-utmp-runlevel.service'], 'ENABLE_UTMP'], ['systemd-user-sessions.service', '8', ['systemd-user-sessions'], 'HAVE_PAM'], + ['systemd-userdbd.service', '8', ['systemd-userdbd'], 'ENABLE_USERDB'], ['systemd-vconsole-setup.service', '8', ['systemd-vconsole-setup'], diff --git a/man/systemd-userdbd.service.xml b/man/systemd-userdbd.service.xml new file mode 100644 index 0000000000..9d5841c0f0 --- /dev/null +++ b/man/systemd-userdbd.service.xml @@ -0,0 +1,69 @@ + + + + + + + + systemd-userdbd.service + systemd + + + + systemd-userdbd.service + 8 + + + + systemd-userdbd.service + systemd-userdbd + JSON User/Group Record Query Multiplexer/NSS Compatibility + + + + systemd-userdbd.service + /usr/lib/systemd/systemd-userdbd + + + + Description + + systemd-userdbd is a system service that multiplexes user/group lookups to all + local services that provide JSON user/group record definitions to the system. In addition it synthesizes + JSON user/group records from classic UNIX/glibc NSS user/group records in order to provide full backwards + compatibility. + + Most of systemd-userdbd's functionality is accessible through the + userdbctl1 + command. + + The user and group records this service provides access to follow the JSON User Record and JSON Group Record definitions. This service implements the + User/Group Record Lookup API via Varlink, and + multiplexes access other services implementing this API, too. It is thus both server and client of this + API. + + This service provides two distinct Varlink services: + io.systemd.Multiplexer provides a single, unified API for querying JSON user and + group records. Internally it talks to all other user/group record services running on the system in + parallel and forwards any information discovered. This simplifies clients substantially since they need + to talk to a single service only instead of all of them in + parallel. io.systemd.NameSeviceSwitch provides compatibility with classic UNIX/glibc + NSS user records, i.e. converts struct passwd and struct group records as + acquired with APIs such as getpwnam1 to JSON + user/group records, thus hiding the differences between the services as much as possible. + + + + See Also + + systemd1, + nss-systemd8, + userdbctl1, + systemd-homed.service8 + + + From 0ba56d3657b30bf6d4f61f3278df3ace9d3b1d5f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 19 Nov 2019 16:51:27 +0100 Subject: [PATCH 26/29] man: document the new nss-systemd behaviour (This also changes the suggested /etc/nsswitch.conf line to use for hooking up nss-system to use glibc's [SUCCESS=merge] feature so that we can properly merge group membership lists). --- man/nss-systemd.xml | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/man/nss-systemd.xml b/man/nss-systemd.xml index 8fde11867c..e343c406f2 100644 --- a/man/nss-systemd.xml +++ b/man/nss-systemd.xml @@ -18,7 +18,7 @@ nss-systemd libnss_systemd.so.2 - Provide UNIX user and group name resolution for dynamic users and groups. + Provide UNIX user and group name resolution for user/group lookup via Varlink @@ -28,16 +28,24 @@ Description - nss-systemd is a plug-in module for the GNU Name Service Switch (NSS) functionality of the - GNU C Library (glibc), providing UNIX user and group name resolution for dynamic users and - groups allocated through the DynamicUser= option in systemd unit files. See - systemd.exec5 for details on - this option. + nss-systemd is a plug-in module for the GNU Name Service Switch (NSS) + functionality of the GNU C Library (glibc), providing UNIX user and group name + resolution for services implementing the User/Group Record + Lookup API via Varlink, such as the system and service manager + systemd1 (for its + DynamicUser= feature, see + systemd.exec5 for + details) or + systemd-homed.service8. This module also ensures that the root and nobody users and groups (i.e. the users/groups with the UIDs/GIDs 0 and 65534) remain resolvable at all times, even if they aren't listed in /etc/passwd or /etc/group, or if these files are missing. + This module preferably utilizes + systemd-userdbd.service8 + for resolving users and groups, but also works without the service running. + To activate the NSS module, add systemd to the lines starting with passwd: and group: in /etc/nsswitch.conf. @@ -54,7 +62,7 @@ passwd: compat mymachines systemd -group: compat mymachines systemd +group: compat [SUCCESS=merge] mymachines [SUCCESS=merge] systemd shadow: compat hosts: files mymachines resolve [!UNAVAIL=return] dns myhostname From 812862db711670712d7e6fa91d61a9e81763d982 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 27 Aug 2019 15:57:33 +0200 Subject: [PATCH 27/29] docs: add documentation for JSON user records --- docs/USER_RECORD.md | 1023 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1023 insertions(+) create mode 100644 docs/USER_RECORD.md diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md new file mode 100644 index 0000000000..a12870dd42 --- /dev/null +++ b/docs/USER_RECORD.md @@ -0,0 +1,1023 @@ +--- +title: JSON User Records +category: Interfaces +layout: default +--- + +# JSON User Records + +systemd optionally processes user records that go beyond the classic UNIX (or +glibc NSS) `struct passwd`. Various components of systemd are able to provide +and consume records in a more extensible format of a dictionary of key/value +pairs, encoded as JSON. Specifically: + +1. [`systemd-homed.service`](https://www.freedesktop.org/software/systemd/man/systemd-homed.service.html) + manages `human` user home directories and embeds these JSON records + directly in the home directory images (see [Home + Directories](https://systemd.io/HOME_DIRECTORY)) for details. + +2. [`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html) + processes these JSON records for users that log in, and applies various + settings to the activated session, including environment variables, nice + levels and more. + +3. [`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html) + processes these JSON records of users that log in, and applies various + resource management settings to the per-user slice units it manages. This + allows setting global limits on resource consumption by a specific user. + +4. [`nss-systemd`](https://www.freedesktop.org/software/systemd/man/nss-systemd.html) + is a glibc NSS module that synthesizes classic NSS records from these JSON + records, providing full backwards compatibility with the classic UNIX APIs + both for look-up and enumeration. + +5. The service manager (PID 1) exposes dynamic users (i.e. users synthesized as + effect of `DynamicUser=` in service unit files) as these advanced JSON + records, making them discoverable to the rest of the system. + +6. [`systemd-userdbd.service`](https://www.freedesktop.org/software/systemd/man/systemd-userdbd.service.html) + is a small service that can translate UNIX/glibc NSS records to these JSON + user records. It also provides a unified [Varlink](https://varlink.org/) API + for querying and enumerating records of this type, optionally acquiring them + from various other services. + +JSON user records may contain various fields that are not available in `struct +passwd`, and are extensible for other applications. For example, the record may +contain information about: + +1. Additional security credentials (PKCS#11 security token information, + biometrical authentication information, SSH public key information) + +2. Additional user metadata, such as a picture, email address, location string, + preferred language or timezone + +3. Resource Management settings (such as CPU/IO weights, memory and tasks + limits, classic UNIX resource limits or nice levels) + +4. Runtime parameters such as environment variables or the `nodev`, `noexec`, + `nosuid` flags to use for the home directory + +5. Information about where to mount the home directory from + +And various other things. The record is intended to be extensible, for example +the following extensions are envisioned: + +1. Windows network credential information + +2. Information about default IMAP, SMTP servers to use for this user + +3. Parental control information to enforce on this user + +4. Default parameters for backup applications and similar + +Similar to JSON User Records there are also [JSON Group +Records](https://systemd.io/GROUP_RECORD.md) that encapsulate UNIX groups. + +JSON User Records may be transferred or written to disk in various protocols +and formats. To inquire about such records defined on the local system use the +[User/Group Lookup API via Varlink](https://systemd.io/USER_GROUP_API.md). + +## Why JSON? + +JSON is nicely extensible and widely used. In particular it's easy to +synthesize and process with numerous programming languages. It's particularly +popular in the web communities, which hopefully should make it easy to link +user credential data from the web and from local systems more closely together. + +## General Structure + +The JSON user records generated and processed by systemd follow a general +structure, consisting of seven distinct "sections". Specifically: + +1. Various fields are placed at the top-level of user record (the `regular` + section). These are generally fields that shall apply unconditionally to the + user in all contexts, are portable and not security sensitive. + +2. A number of fields are located in the `privileged` section (a sub-object of + the user record). Fields contained in this object are security sensitive, + i.e. contain information that the user and the administrator should be able + to see, but other users should not. In many ways this matches the data + stored in `/etc/shadow` in classic Linux user accounts, i.e. includes + password hashes and more. Algorithmically, when a user record is passed to + an untrusted client, by monopolizing such sensitive records in a single + object field we can easily remove it from view. + +3. A number of fields are located in objects inside the `perMachine` section + (an array field of the user record). Primarily these are resource + management-related fields, as those tend to make sense on a specific system + only, e.g. limiting a user's memory use to 1G only makes sense on a specific + system that has more than 1G of memory. Each object inside the `perMachine` + array comes with a `matchMachineId` or `matchHostname` field which indicate + which systems to apply the listed settings to. Note that many fields + accepted in the `perMachine` section can also be set at the top level (the + `regular` section), where they define the fallback if no matching object in + `perMachine` is found. + +4. Various fields are located in the `binding` section (a sub-sub-object of the + user record; an intermediary object is inserted which is keyed by the + machine ID of the host). Fields included in this section "bind" the object + to a specific system. They generally include non-portable information about + paths or UID assignments, that are true on a specific system, but not + necessarily on others, and which are managed automatically by some user + record manager (such as `systemd-homed`). Data in this section is considered + part of the user record only in the local context, and is generally not + ported to other systems. Due to that it is not included in the reduced user + record the cryptographic signature defined in the `signature` section is + calculated on. In `systemd-homed` this section is also removed when the + user's record is stored in the `~/.identity` file in the home directory, so + that every system with access to the home directory can manage these + `binding` fields individually. Typically, the binding section is persisted + to the local disk. + +5. Various fields are located in the `status` section (a sub-sub-object of the + user record, also with an intermediary object between that is keyed by the + machine ID, similar to the way the `binding` section is organized). This + section is augmented during runtime only, and never persisted to disk. The + idea is that this section contains information about current runtime + resource usage (for example: currently used disk space of the user), that + changes dynamically but is otherwise immediately associated with the user + record and for many purposes should be considered to be part of the user + record. + +6. The `signature` section contains one or more cryptographic signatures of a + reduced version of the user record. This is used to ensure that only user + records defined by a specific source are accepted on a system, by validating + the signature against the set of locally accepted signature public keys. The + signature is calculated from the JSON user record with all sections removed, + except for `regular`, `privileged`, `perMachine`. Specifically, `binding`, + `status`, `signature` itself and `secret` are removed first and thus not + covered by the signature. This section is optional, and is only used when + cryptographic validation of user records is required (as it is by + `systemd-homed.service` for example). + +7. The `secret` section contains secret user credentials, such as password or + PIN information. This data is never persisted, and never returned when user + records are inquired by a client, privileged or not. This data should only + be included in a user record very briefly, for example when certain very + specific operations are executed. For example, in tools such as + `systemd-homed` this section may be included in user records, when creating + a new home directory, as passwords and similar credentials need to be + provided to encrypt the home directory with. + +Here's a tabular overview of the sections and their properties: + +| Section | Included in Signature | Persistent | Security Sensitive | Contains Host-Specific Data | +|------------|-----------------------|------------|--------------------|-----------------------------| +| regular | yes | yes | no | no | +| privileged | yes | yes | yes | no | +| perMachine | yes | yes | no | yes | +| binding | no | yes | no | yes | +| status | no | no | no | yes | +| signature | no | yes | no | no | +| secret | no | no | yes | no | + +Note that services providing user records to the local system are free to +manage only a subset of these sections and never include the others in +them. For example, a service that has no concept of signed records (for example +because the records it manages are inherently trusted anyway) does not have to +bother with the `signature` section. A service that only defines records in a +strictly local context and without signatures doesn't have to deal with the +`perMachine` or `binding` sections and can include its data exclusively in the +regular section. A service that uses a separate, private channel for +authenticating users (or that doesn't have a concept of authentication at all) +does not need to to be concerned with the `secret` section of user records, as +the fields included therein are only useful when executing authentication +operations natively against JSON user records. + +The `systemd-homed' manager uses all seven sections for various +purposes. Inside the home directories (and if the LUKS2 backend is used, also +in the LUKS2 header) a user record containing the `regular`, `privileged`, +`perMachine` and `signature` sections is stored. `systemd-homed` also stores a +version of the record on the host, with the same four sections and augmented +with an additional, fifth `binding` section. When a local client enquires about +a user record managed by `systemd-homed` the service will add in some +additional information about the user and home directory in the `status` +section — this version is only transferred via IPC and never written to +disk. Finally the `secret` section is used during authentication operations via +IPC to transfer the user record along with its authentication tokens in one go. + +## Fields in the `regular` section + +As mentioned, the `regular` section's fields are placed at the top level +object. The following fields are currently defined: + +`userName` → The UNIX user name for this record. Takes a string with a valid +UNIX user name. This field is the only mandatory field, all others are +optional. Corresponds with the `pw_name` field of of `struct passwd` and the +`sp_namp` field of `struct spwd` (i.e. the shadow user record stored in +`/etc/shadow`). + +`realm` → The "realm" a user is defined in. This concept allows distinguishing +users with the same name that originate in different organizations or +installations. This should take a string in DNS domain syntax, but doesn't have +to refer to an actual DNS domain (though it is recommended to use one for +this). The idea is that the user `lpoetter` in the `redhat.com` realm might be +distinct from the same user in the `poettering.hq` realm. User records for the +same user name that have different realm fields are considered referring to +different users. When updating a user record it is required that any new +version has to match in both `userName` and `realm` field. This field is +optional, when unset the user should not be considered part of any realm. A +user record with a realm set is never compatible (for the purpose of updates, +see above) with a user record without one set, even if the `userName` field matches. + +`realName` → The real name of the user, a string. This should contain the user's +real ("human") name, and corresponds loosely to the GECOS field of classic UNIX +user records. When converting a `struct passwd` to a JSON user record this +field is initialized from GECOS (i.e. the `pw_gecos` field), and vice versa +when converting back. That said, unlike GECOS this field is supposed to contain +only the real name and no other information. + +`emailAddress` → The email address of the user, formatted as +string. [`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html) +initializes the `$EMAIL` environment variable from this value for all login +sessions. + +`iconName` → The name of an icon picked by the user, for example for the +purpose of an avatar. This must be a string, and should follow the semantics +defined in the [Icon Naming +Specification](https://standards.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html). + +`location` → A free-form location string describing the location of the user, +if that is applicable. It's probably wise to use a location string processable +by geo-location subsystems, but this is not enforced nor required. Example: +`Berlin, Germany` or `Basement, Room 3a`. + +`disposition` → A string, one of `intrinsic`, `system`, `dynamic`, `regular`, +`container`, `reserved`. If specified clarifies the disposition of the user, +i.e. the context it is defined in. For regular, "human" users this should be +`regular`, for system users (i.e. users that system services run under, and +similar) this should be `system`. The `intrinsic` disposition should be used +only for the two users that have special meaning to the OS kernel itself, +i.e. the `root` and `nobody` users. The `container` string should be used for +users that are used by an OS container, and hence will show up in `ps` listings +and such, but are only defined in container context. Finally `reserved` should +be used for any users outside of these use-cases. Note that this property is +entirely optional and applications are assumed to be able to derive the +disposition of a user automatically from a record even in absence of this +field, based on other fields, for example the numeric UID. By setting this +field explicitly applications can override this default determination. + +`lastChangeUSec` → An unsigned 64bit integer value, referring to a timestamp in µs +since the epoch 1970, indicating when the user record (specifically, any of the +`regular`, `privileged`, `perMachine` sections) was last changed. This field is +used when comparing two records of the same user to identify the newer one, and +is used for example for automatic updating of user records, where appropriate. + +`lastPasswordChangeUSec` → Similar, also an unsigned 64bit integer value, +indicating the point in time the password (or any authentication token) of the +user was last changed. This corresponds to the `sp_lstchg` field of `struct +spwd`, i.e. the matching field in the user shadow database `/etc/shadow`, +though provides finer resolution. + +`shell` → A string, referring to the shell binary to use for terminal logins of +this user. This corresponds with the `pw_shell` field of `struct passwd`, and +should contain an absolute file system path. For system users not suitable for +terminal log-in this field should not be set. + +`umask` → The `umask` to set for the user's login sessions. Takes an +integer. Note that usually on UNIX the umask is noted in octal, but JSON's +integers are generally written in decimal, hence in this context we denote it +umask in decimal too. The specified value should be in the valid range for +umasks, i.e. 0000…0777 (in octal as typical in UNIX), or 0…511 (in decimal, how +it actually appears in the JSON record). This `umask` is automatically set by +[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html) +for all login sessions of the user. + +`environment` → An array of strings, each containing an environment variable +and its value to set for the user's login session, in a format compatible with +[`putenv()`](http://man7.org/linux/man-pages/man3/putenv.3.html). Any +environment variable listed here is automatically set by +[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html) +for all login sessions of the user. + +`timeZone` → A string indicating a preferred timezone to use for the user. When +logging in +[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html) +will automatically initialize the `$TZ` environment variable from this +string. The string should be a `tzdata` compatible location string, for +example: `Europe/Berlin`. + +`preferredLanguage` → A string indicating the preferred language/locale for the +user. When logging in +[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html) +will automatically initialize the `$LANG` environment variable from this +string. The string hence should be in a format compatible with this environment +variable, for example: `de_DE.UTF8`. + +`niceLevel` → An integer value in the range -20…19. When logging in +[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html) +will automatically initialize the login process' nice level to this value with, +which is then inherited by all the user's processes, see +[`setpriority()`](http://man7.org/linux/man-pages/man2/setpriority.2.html) for +more information. + +`resourceLimits` → An object, where each key refers to a Linux resource limit +(such as `RLIMIT_NOFILE` and similar). Their values should be an object with +two keys `cur` and `max` for the soft and hard resource limit. When logging in +[`pam_systemd`](https://www.freedesktop.org/software/systemd/man/pam_systemd.html) +will automatically initialize the login process' resource limits to these +values, which is then inherited by all the user's processes, see +[`setrlimit()`](http://man7.org/linux/man-pages/man2/setrlimit.2.html) for more +information. + +`locked` → A boolean value. If true the user account is locked, the user may +not log in. If this field is missing it should be assumed to be false, +i.e. logins are permitted. This field corresponds to the `sp_expire` field of +`struct spwd` (i.e. the `/etc/shadow` data for a user) being set to zero or +one. + +`notBeforeUSec` → An unsigned 64bit integer value, indicating a time in µs since +the UNIX epoch (1970) before which the record should be considered invalid for +the purpose of logging in. + +`notAfterUSec` → Similar, but indicates the point in time *after* which logins +shall not be permitted anymore. This corresponds to the `sp_expire` field of +`struct spwd`, when it is set to a value larger than one, but provides finer +granularity. + +`storage` → A string, one of `classic`, `luks`, `directory`, `subvolume`, +`fscrypt`, `cifs`. Indicates the storage mechanism for the user's home +directory. If `classic` the home directory is a plain directory as in classic +UNIX. When `directory`, the home directory is a regular directory, but the +`~/.identity` file in it contains the user's user record, so that the directory +is self-contained. Similar, `subvolume` is a `btrfs` subvolume that also +contains a `~/.identity` user record; `fscrypt` is an `fscrypt`-encrypted +directory, also containing the `~/.identity` user record; `luks` is a per-user +LUKS volume that is mounted as home directory, and `cifs` a home directory +mounted from a Windows File Share. The five latter types are primarily used by +`systemd-homed` when managing home directories, but may be used if other +managers are used too. If this is not set `classic` is the implied default. + +`diskSize` → An unsigned 64bit integer, indicating the intended home directory +disk space in bytes to assign to the user. Depending on the selected storage +type this might be implement differently: for `luks` this is the intended size +of the file system and LUKS volume, while for the others this likely translates +to classic file system quota settings. + +`diskSizeRelative` → Similar to `diskSize` but takes a relative value, but +specifies a fraction of the available disk space on the selected storage medium +to assign to the user. This unsigned integer value is normalized to 2^32 = +100%. + +`skeletonDirectory` → Takes a string with the absolute path to the skeleton +directory to populate a new home directory from. This is only used when a home +directory is first created, and defaults to `/etc/skel` if not defined. + +`accessMode` → Takes an unsigned integer in the range 0…511 indicating the UNIX +access mask for the home directory when it is first created. + +`tasksMax` → Takes an unsigned 64bit integer indicating the maximum number of +tasks the user may start in parallel during system runtime. This value is +enforced on all tasks (i.e. processes and threads) the user starts or that are +forked off these processes regardless if the change user identity (for example +by setuid binaries/`su`/`sudo` and +similar). [`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html) +enforces this by setting the `TasksMax` slice property for the user's slice +`user-$UID.slice`. + +`memoryHigh`/`memoryMax` → These take unsigned 64bit integers indicating upper +memory limits for all processes of the user (plus all processes forked off them +that might have changed user identity), in bytes. Enforced by +[`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html), +similar to `tasksMax`. + +`cpuWeight`/`ioWeight` → These take unsigned integers in the range 100…10000 +and configure the CPU and IO scheduling weights for the user's processes as a +whole. Also enforced by +[`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html), +similar to `tasksMax`, `memoryHigh` and `memoryMax`. + +`mountNoDevices`/`mountNoSuid`/`mountNoExecute` → Three booleans that control +the `nodev`, `nosuid`, `noexec` mount flags of the user's home +directories. Note that these booleans are only honored if the home directory +is managed by a subsystem such as `systemd-homed.service` that automatically +mounts home directories on login. + +`cifsDomain` → A string indicating the Windows File Sharing domain (CIFS) to +use. This is generally useful, but particularly when `cifs` is used as storage +mechanism for the user's home directory, see above. + +`cifsUserName` → A string indicating the Windows File Sharing user name (CIFS) +to associate this user record with. This is generally useful, but particularly +useful when `cifs` is used as storage mechanism for the user's home directory, +see above. + +`cifsService` → A string indicating the Windows File Share service (CIFS) to +mount as home directory of the user on login. + +`imagePath` → A string with an absolute file system path to the file, directory +or block device to use for storage backing the home directory. If the `luks` +storage is used this refers to the loopback file or block device node to store +the LUKS volume on. For `fscrypt`, `directory`, `subvolume` this refers to the +directory to bind mount as home directory on login. Not defined for `classic` +or `cifs`. + +`homeDirectory` → A string with an absolute file system path to the home +directory. This is where the image indicated in `imagePath` is mounted to on +login and thus indicates the application facing home directory while the home +directory is active, and is what the user's `$HOME` environment variable is set +to during log-in. It corresponds to the `pw_dir` field of `struct passwd`. + +`uid` → An unsigned integer in the range 0…4294967295: the numeric UNIX user ID (UID) to +use for the user. This corresponds to the `pw_uid` field of `struct passwd`. + +`gid` → An unsigned integer in the range 0…4294967295: the numeric UNIX group +ID (GID) to use for the user. This corresponds to the `pw_gid` field of +`struct passwd`. + +`memberOf` → An array of strings, each indicating a UNIX group this user shall +be a member of. The listed strings must be valid group names, but it is not +required that all groups listed exist in all contexts: any entry for which no +group exists should be silently ignored. + +`fileSystemType` → A string, one of `ext4`, `xfs`, `btrfs` (possibly others) to +use as file system for the user's home directory. This is primarily relevant +when the storage mechanism used is `luks` as a file system to use inside the +LUKS container must be selected. + +`partitionUuid` → A string containing a lower-case, text-formatted UUID, referencing +the GPT partition UUID the home directory is located in. This is primarily +relevant when the storage mechanism used is `luks`. + +`luksUuid` → A string containing a lower-case, text-formatted UUID, referencing +the LUKS volume UUID the home directory is located in. This is primarily +relevant when the storage mechanism used is `luks`. + +`fileSystemUuid` → A string containing a lower-case, text-formatted UUID, +referencing the file system UUID the home directory is located in. This is +primarily relevant when the storage mechanism used is `luks`. + +`luksDiscard` → A boolean. If true and `luks` storage is used controls whether +the loopback block devices, LUKS and the file system on top shall be used in +`discard` mode, i.e. erased sectors should always be returned to the underlying +storage. If false and `luks` storage is used turns this behavior off. In +addition, depending on this setting an `FITRIM` or `fallocate()` operation is +executed to make sure the image matches the selected option. + +`luksCipher` → A string, indicating the cipher to use for the LUKS storage mechanism. + +`luksCipherMode` → A string, selecting the cipher mode to use for the LUKS storage mechanism. + +`luksVolumeKeySize` → An unsigned integer, indicating the volume key length in +bytes to use for the LUKS storage mechanism. + +`luksPbkdfHashAlgorithm` → A string, selecting the hash algorithm to use for +the PBKDF operation for the LUKS storage mechanism. + +`luksPbkdfType` → A string, indicating the PBKDF type to use for the LUKS storage mechanism. + +`luksPbkdfTimeCostUSec` → An unsigned 64bit integer, indicating the intended +time cost for the PBKDF operation, when the LUKS storage mechanism is used, in +µs. + +`luksPbkdfMemoryCost` → An unsigned 64bit integer, indicating the intended +memory cost for the PBKDF operation, when LUKS storage is used, in bytes. + +`luksPbkdfParallelThreads` → An unsigned 64bit integer, indicating the intended +required parallel threads for the PBKDF operation, when LUKS storage is used. + +`service` → A string declaring the service that defines or manages this user +record. It is recommended to use reverse domain name notation for this. For +example, if `systemd-homed` manages a user a string of `io.systemd.Home` is +used for this. + +`rateLimitIntervalUSec` → An unsigned 64bit integer that configures the +authentication rate limiting enforced on the user account. This specifies a +timer interval (in µs) within which to count authentication attempts. When the +counter goes above the value configured n `rateLimitIntervalBurst` log-ins are +temporarily refused until the interval passes. + +`rateLimitIntervalBurst` → An unsigned 64bit integer, closely related to +`rateLimitIntervalUSec`, that puts a limit on authentication attempts within +the configured time interval. + +`enforcePasswordPolicy` → A boolean. Configures whether to enforce the system's +password policy when creating the home directory for the user or changing the +user's password. By default the policy is enforced, but if this field is false +it is bypassed. + +`autoLogin` → A boolean. If true the user record is marked as suitable for +auto-login. Systems are supposed to automatically log in a user marked this way +during boot, if there's exactly one user on it defined this way. + +`stopDelayUSec` → An unsigned 64bit integer, indicating the time in µs the +per-user service manager is kept around after the user fully logged out. This +value is honored by +[`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html). If +set to zero the per-user service manager is immediately terminated when the +user logs out, and longer values optimize high-frequency log-ins as the +necessary work to set up and tear down a log-in is reduced if the service +manager stays running. + +`killProcesses` → A boolean. If true all processes of the user are +automatically killed when the user logs out. This is enforced by +[`systemd-logind.service`](https://www.freedesktop.org/software/systemd/man/systemd-logind.service.html). If +false any processes left around when the user logs out are left running. + +`passwordChangeMinUSec`/`passwordChangeMaxUSec` → An unsigned 64bit integer, +encoding how much time has to pass at least/at most between password changes of +the user. This corresponds with the `sp_min` and `sp_max` fields of `struct +spwd` (i.e. the `/etc/shadow` entries of the user), but offers finer +granularity. + +`passwordChangeWarnUSec` → An unsigned 64bit integer, encoding how much time to +warn the user before their password expires, in µs. This corresponds with the +`sp_warn` field of `struct spwd`. + +`passwordChangeInactiveUSec` → An unsigned 64bit integer, encoding how much +time has to pass after the password expired that the account is +deactivated. This corresponds with the `sp_inact` field of `struct spwd`. + +`passwordChangeNow` → A boolean. If true the user has to change their password +on next login. This corresponds with the `sp_lstchg` field of `struct spwd` +being set to zero. + +`pkcs11TokenUri` → An array of strings, each with an RFC 7512 compliant PKCS#11 +URI referring to security token (or smart card) of some form, that shall be +associated with the user and may be used for authentication. The URI is used to +search for an X.509 certificate and associated private key that may be used to +decrypt an encrypted secret key that is used to unlock the user's account (see +below). It's undefined how precise the URI is: during log-in it is tested +against all plugged in security tokens and if there's exactly one matching +private key found with it it is used. + +`privileged` → An object, which contains the fields of he `privileged` section +of the user record, see below. + +`perMachine` → An array of objects, which contain the `perMachine` section of +the user record, and thus fields to apply on specific systems only, see below. + +`binding` → An object, keyed by machine IDs formatted as strings, pointing +to objects that contain the `binding` section of the user record, +i.e. additional fields that bind the user record to a specific machine, see +below. + +`status` → An object, keyed by machine IDs formatted as strings, pointing to +objects that contain the `status` section of the user record, i.e. additional +runtime fields that expose the current status of the user record on a specific +system, see below. + +`signature` → An array of objects, which contain cryptographic signatures of +the user record, i.e. the fields of the `signature` section of the user record, +see below. + +`secret` → An object, which contains the fields of the `secret` section of the +user record, see below. + +## Fields in the `privileged` section + +As mentioned, the `privileged` section is encoded in a sub-object of the user +record top-level object, in the `privileged` field. Any data included in this +object shall only be visible to the administrator and the user themselves, and +be suppressed implicitly when other users get access to a user record. It thus +takes the role of the `/etc/shadow` records for each user, which has similarly +restrictive access semantics. The following fields are currently defined: + +`passwordHint` → A user-selected password hint in free-form text. This should +be a string like "What's the name of your first pet?", but is entirely for the +user to choose. + +`hashPassword` → An array of strings, each containing a hashed UNIX password +string, in the format +[`crypt(3)`](http://man7.org/linux/man-pages/man3/crypt.3.html) generates. This +corresponds with `sp_pwdp` field of `struct spwd` (and in a way the `pw_passwd` +field of `struct passwd`). + +`sshAuthorizedKeys` → An array of strings, each listing an SSH public key that +is authorized to access the account. The strings should follow the same format +as the lines in the traditional `~/.ssh/authorized_key` file. + +`pkcs11EncryptedKey` → An array of objects. Each element of the array should be +an object consisting of three string fields: `uri` shall contain a PKCS#11 +security token URI, `data` shall contain a Base64 encoded encrypted key and +`hashedPassword` shall contain a UNIX password hash to test the key +against. Authenticating with a security token against this account shall work +as follows: the encrypted secret key is converted from its Base64 +representation into binary, then decrypted with the PKCS#11 `C_Decrypt()` +function of the PKCS#11 module referenced by the specified URI, using the +private key found on the same token. The resulting decrypted key is then +Base64-encoded and tested against the specified UNIX hashed password. The +Base64-enceded decrypted key may also be used to unlock further resources +during log-in, for example the LUKS or `fscrypt` storage backend. It is +generally recommended that for each entry in `pkcs11EncryptedKey` there's also +a matching one in `pkcs11TokenUri` and vice versa, with the same URI, appearing +in the same order, but this should not be required by applications processing +user records. + +## Fields in the `perMachine` section + +As mentioned, the `perMachine` section contains settings that shall apply to +specific systems only. This is primarily interesting for resource management +properties as they tend to require a per-system focus, however they may be used +for other purposes too. + +The `perMachine` field in the top-level object is an array of objects. When +processing the user record first the various fields on the top-level object +should be used. Then this array should be iterated in order, and the various +settings be applied that match either the indicated machine ID or host +name. There may be multiple array entries that match a specific system, in +which case all the object's setting should be applied. If the same option is +set in the top-level object as in a per-machine object the latter wins and +entirely undoes the setting in the top-level object (i.e. no merging of +properties that are arrays themselves is done). If the same option is set in +multiple per-machine objects the one specified later in the array wins (and +here too no merging of individual fields is done, the later field always wins +in full). + +The following fields are defined in this section: + +`matchMachineId` → An array of strings with each a formatted 128bit ID in +hex. If any of the specified IDs match the system's local machine ID +(i.e. matches `/etc/machine-id`) the fields in this object are honored. + +`matchHostname` → An array of string with a each a valid hostname. If any of +the specified hostnames match the system's local hostname, the fields in this +object are honored. If both `matchHostname` and `matchMachineId` are used +within the same array entry, the object is honored when either match succeeds, +i.e. the two match types are combined in OR, not in AND. + +These two are the only two fields specific to this section. All other fields +that may be used in this section are identical to the equally named ones in the +`regular` section (i.e. at the top-level object). Specifically, these are: + +`iconName`, `location`, `shell`, `umask`, `environment`, `timeZone`, +`preferredLanguage`, `niceLevel`, `resourceLimits`, `locked`, `notBeforeUSec`, +`notAfterUSec`, `storage`, `diskSize`, `diskSizeRelative`, `skeletonDirectory`, +`accessMode`, `tasksMax`, `memoryHigh`, `memoryMax`, `cpuWeight`, `ioWeight`, +`mountNoDevices`, `mountNoSuid`, `mountNoExecute`, `cifsDomain`, +`cifsUserName`, `cifsService`, `imagePath`, `uid`, `gid`, `memberOf`, +`fileSystemType`, `partitionUuid`, `luksUuid`, `fileSystemUuid`, `luksDiscard`, +`luksCipher`, `luksCipherMode`, `luksVolumeKeySize`, `luksPbkdfHashAlgorithm`, +`luksPbkdfType`, `luksPbkdfTimeCostUSec`, `luksPbkdfMemoryCost`, +`luksPbkdfParallelThreads`, `rateLimitIntervalUSec`, `rateLimitBurst`, +`enforcePasswordPolicy`, `autoLogin`, `stopDelayUSec`, `killProcesses`, +`passwordChangeMinUSec`, `passwordChangeMaxUSec`, `passwordChangeWarnUSec`, +`passwordChangeInactiveUSec`, `passwordChangeNow`, `pkcs11TokenUri`. + +## Fields in the `binding` section + +As mentioned, the `binding` section contains additional fields about the user +record, that bind it to the local system. These fields are generally used by a +local user manager (such as `systemd-homed.service`) to add in fields that make +sense in a local context but not necessarily in a global one. For example, a +user record that contains no `uid` field in the regular section is likely +extended with one in the `binding` section to assign a local UID if no global +UID is defined. + +All fields in the `binding` section only make sense in a local context and are +suppressed when the user record is ported between systems. The `binding` section +is generally persisted on the system but not in the home directories themselves +and the home directory is supposed to be fully portable and thus not contain +the information that `binding` is supposed to contain that binds the portable +record to a specific system. + +The `binding` sub-object on the top-level user record object is keyed by the +machine ID the binding is intended for, which point to an object with the +fields of the bindings. These fields generally match fields that may also be +defined in the `regular` and `perMachine` sections, however override +both. Usually, the `binding` value should not contain settings different from +those set via `regular` or `perMachine`, however this might happen if some +settings are not supported locally (think: `fscrypt` is recorded as intended +storage mechanism in the `regular` section, but the local kernel does not +support `fscrypt`, hence `directory` was chosen as implicit fallback), or have +been changed in the `regular` section through updates (e.g. a home directory +was created with `luks` as storage mechanism but later the user record was +updated to prefer `subvolume`, which however doesn't change the actual storage +used already which is pinned in the `binding` section). + +The following fields are defined in the `binding` section. They all have an +identical format and override their equally named counterparts in the `regular` +and `perMachine` sections: + +`imagePath`, `homeDirectory`, `partitionUuid`, `luksUuid`, `fileSystemUuid`, +`uid`, `gid`, `storage`, `fileSystemType`, `luksCipher`, `luksCipherMode`, +`luksVolumeKeySize`. + +## Fields in the `status` section + +As mentioned, the `status` section contains additional fields about the user +record that are exclusively acquired during runtime, and that expose runtime +metrics of the user and similar metadata that shall not be persisted but are +only acquired "on-the-fly" when requested. + +This section is arranged similarly to the `binding` section: the `status` +sub-object of the top-level user record object is keyed by the machine ID, +which points to the object with the fields defined here. The following fields +are defined: + +`diskUsage` → An unsigned 64bit integer. The currently used disk space of the +home directory in bytes. This value might be determined in different ways, +depending on the selected storage mechanism. For LUKS storage this is the file +size of the loopback file or block device size. For the +directory/subvolume/fscrypt storage this is the current disk space used as +reported by the file system quota subsystem. + +`diskFree` → An unsigned 64bit integer, denoting the number of "free" bytes in +the disk space allotment, i.e. usually the difference between the disk size as +reported by `diskSize` and the used already as reported in `diskFree`, but +possibly skewed by metadata sizes, disk compression and similar. + +`diskSize` → An unsigned 64bit integer, denoting the disk space currently +allotted to the user, in bytes. Depending on the storage mechanism this can mean +different things (see above). In contrast to the top-level field of the same +(or the one in the `perMachine` section), this field reports the current size +allotted to the user, not the intended one. The values may differ when user +records are updated without the home directory being re-sized. + +`diskCeiling`/`diskFloor` → Unsigned 64bit integers indicating upper and lower +bounds when changing the `diskSize` value, in bytes. These values are typically +derived from the underlying data storage, and indicate in which range the home +directory may be re-sized in, i.e. in which sensible range the `diskSize` value +should be kept. + +`state` → A string indicating the current state of the home directory. The +precise set of values exposed here are up to the service managing the home +directory to define (i.e. are up to the service identified with the `service` +field below). However, it is recommended to stick to a basic vocabulary here: +`inactive` for a home directory currently not mounted, `absent` for a home +directory that cannot be mounted currently because it does not exist on the +local system, `active` for a home directory that is currently mounted and +accessible. + +`service` → A string identifying the service that manages this user record. For +example `systemd-homed.service` sets this to `io.systemd.Home` to all user +records it manages. This is particularly relevant to define clearly the context +in which `state` lives, see above. Note that this field also exists on the +top-level object (i.e. in the `regular` section), which it overrides. The +`regular` field should be used if conceptually the user record can only be +managed by the specified service, and this `status` field if it can +conceptually be managed by different managers, but currently is managed by the +specified one. + +`signedLocally` → A boolean. If true indicates that the user record is signed +by a public key for which the private key is available locally. This means that +the user record may be modified locally as it can be re-signed with the private +key. If false indicates that the user record is signed by a public key +recognized by the local manager but whose private key is not available +locally. This means the user record cannot be modified locally as it couldn't +be signed afterwards. + +`goodAuthenticationCounter` → An unsigned 64bit integer. This counter is +increased by one on every successful authentication attempt, i.e. an +authentication attempt where a security token of some form was presented and it +was correct. + +`badAuthenticationCounter` → An unsigned 64bit integer. This counter is +increased by one on every unsuccessfully authentication attempt, i.e. an +authentication attempt where a security token of some form was presented and it +was incorrect. + +`lastGoodAuthenticationUSec` → An unsigned 64bit integer, indicating the time +of the last successful authentication attempt in µs since the UNIX epoch (1970). + +`lastBadAuthenticationUSec` → Similar, but the timestamp of the last +unsuccessfully authentication attempt. + +`rateLimitBeginUSec` → An unsigned 64bit integer: the µs timestamp since the +UNIX epoch (1970) where the most recent rate limiting interval has been +started, as configured with `rateLimitIntervalUSec`. + +`rateLimitCount` → An unsigned 64bit integer, counting the authentication +attempts in the current rate limiting interval, see above. If this counter +grows beyond the value configured in `rateLimitBurst` authentication attempts +are temporarily refused. + +`removable` → A boolean value. If true the manager of this user record +determined the home directory being on removable media. If false it was +determined the home directory is in internal built-in media. (This is used by +`systemd-logind.service` to automatically pick the right default value for +`stopDelayUSec` if the field is not explicitly specified: for home directories +on removable media the delay is selected very low to minimize the chance the +home directory remains in unclean state if the storage device is removed from +the system by the user). + +## Fields in the `signature` section + +As mentioned, the `signature` section of the user record may contain one or +more cryptographic signatures of the user record. Like all others, this section +is optional, and only used when cryptographic validation of user records shall +be used. Specifically, all user records managed by `systemd-homed.service` will +carry such signatures and the service refuses managing user records that come +without signature or with signatures not recognized by any locally defined +public key. + +The `signature` field in the top-level user record object is an array of +objects. Each object encapsulates one signature and has two fields: `data` and +`key` (both are strings). The `data` field contains the actual signature, +encoded in base64, the `key` field contains a copy of the public key whose +private key was used to make the signature, in PEM format. Currently only +signatures with Ed25519 keys are defined. + +Before signing the user record should be brought into "normalized" form, +i.e. the keys in all objects should be sorted alphabetically. All redundant +white-space and newlines should be removed and the JSON text then signed. + +The signatures only cover the `regular`, `perMachine` and `privileged` sections +of the user records, all other sections (include `signature` itself), are +removed before the signature is calculated. + +Rationale for signing and threat model: while a multi-user operating system +like Linux strives for being sufficiently secure even after a user acquired a +local login session reality tells us this is not the case. Hence it is +essential to restrict carefully which users may gain access to a system and +which ones shall not. A minimal level of trust must be established between +system, user record and the user themselves before a log-in request may be +permitted. In particular if the home directory is provided in its own LUKS2 +encapsulated file system it is essential this trust is established before the +user logs in (and hence the file system mounted), since file system +implementations on Linux are well known to be relatively vulnerable to rogue +disk images. User records and home directories in many context are expected to +be something shareable between multiple systems, and the transfer between them +might not happen via exclusively trusted channels. Hence it's essential that +the user record is not manipulated between uses. Finally, resource management +(which may be done by the various fields of the user record) is security +sensitive, since it should forcefully lock the user into the assigned resource +usage and not allow them to use more. The requirement of being able to trust +the user record data combined with the potential transfer over untrusted +channels suggest a cryptographic signature mechanism where only user records +signed by a recognized key are permitted to log in locally. + +Note that other mechanisms for establishing sufficient trust exist too, and are +perfectly valid as well. For example, systems like LDAP/ActiveDirectory +generally insist on user record transfer from trusted servers via encrypted TLS +channels only. Or traditional UNIX users created locally in `/etc/passwd` never +exist outside of the local trusted system, hence transfer and trust in the +source are not an issue. The major benefit of operating with signed user +records is that they are self-sufficiently trusted, not relying on a secure +channel for transfer, and thus being compatible with a more distributed model +of home directory transfer, including on USB sticks and such. + +## Fields in the `secret` section + +As mentioned, the `secret` section of the user record should never be persisted +nor transferred across machines. It is only defined in short-lived operations, +for example when a user record is first created or registered, as the secret +key data needs to be available to derive encryption keys from and similar. + +The `secret` field of the top-level user record contains the following fields: + +`password` → an array of strings, each containing a plain text password. + +`pkcs11Pin` → an array of strings, each containing a plain text PIN, suitable +for unlocking PKCS#11 security tokens that require that. + +`pkcs11ProtectedAuthenticationPathPermitted` → a boolean. If set to true allows +the receiver to use the PKCS#11 "protected authentication path" (i.e. a +physical button/touch element on the security token) for authenticating the +user. If false or unset authentication this way shall not be attempted. + +## Mapping to `struct passwd` and `struct spwd` + +When mapping classic UNIX user records (i.e. `struct passwd` and `struct spwd`) +to JSON user records the following mappings should be applied: + +| Structure | Field | Section | Field | Condition | +|-----------------|-------------|--------------|------------------------------|----------------------------| +| `struct passwd` | `pw_name` | `regular` | `userName` | | +| `struct passwd` | `pw_passwd` | `privileged` | `password` | (See notes below) | +| `struct passwd` | `pw_uid` | `regular` | `uid` | | +| `struct passwd` | `pw_gid` | `regular` | `gid` | | +| `struct passwd` | `pw_gecos` | `regular` | `realName` | | +| `struct passwd` | `pw_dir` | `regular` | `homeDirectory` | | +| `struct passwd` | `pw_shell` | `regular` | `shell` | | +| `struct spwd` | `sp_namp` | `regular` | `userName` | | +| `struct spwd` | `sp_pwdp` | `privileged` | `password` | (See notes below) | +| `struct spwd` | `sp_lstchg` | `regular` | `lastPasswordChangeUSec` | (if `sp_lstchg` > 0) | +| `struct spwd` | `sp_lstchg` | `regular` | `passwordChangeNow` | (if `sp_lstchg` == 0) | +| `struct spwd` | `sp_min` | `regular` | `passwordChangeMinUSec` | | +| `struct spwd` | `sp_max` | `regular` | `passwordChangeMaxUSec` | | +| `struct spwd` | `sp_warn` | `regular` | `passwordChangeWarnUSec` | | +| `struct spwd` | `sp_inact` | `regular` | `passwordChangeInactiveUSec` | | +| `struct spwd` | `sp_expire` | `regular` | `locked` | (if `sp_expire` in [0, 1]) | +| `struct spwd` | `sp_expire` | `regular` | `notAfterUSec` | (if `sp_expire` > 1) | + +At this time almost all Linux machines employ shadow passwords, thus the +`pw_passwd` field in `struct passwd` is set to `"x"`, and the actual password +is stored in the shadow entry `struct spwd`'s field `sp_pwdp`. + +## Extending These Records + +User records following this specifications are supposed to be extendable for +various applications. In general, subsystems are free to introduce their own +keys, as long as: + +* Care should be taken to place the keys in the right section, i.e. the most + appropriate for the data field. + +* Care should be taken to avoid namespace clashes. Please prefix your fields + with a short identifier of your project to avoid ambiguities and + incompatibilities. + +* This specification is supposed to be a living specification. If you need + additional fields, please consider submitting them upstream for inclusion in + this specification. If they are reasonably universally useful, it would be + best to list them here. + +## Examples + +The shortest valid user record looks like this: + +```json +{ + "userName" : "u" +} +``` + +A reasonable user record for a system user might look like this: + +```json +{ + "userName" : "httpd", + "uid" : 473, + "gid" : 473, + "disposition" : "system", + "locked" : true +} +``` + +A fully featured user record associated with a home directory managed by +`systemd-homed.service` might look like this: + +```json +{ + "autoLogin" : true, + "binding" : { + "15e19cf24e004b949ddaac60c74aa165" : { + "fileSystemType" : "ext4", + "fileSystemUuid" : "758e88c8-5851-4a2a-b88f-e7474279c111", + "gid" : 60232, + "homeDirectory" : "/home/grobie", + "imagePath" : "/home/grobie.home", + "luksCipher" : "aes", + "luksCipherMode" : "xts-plain64", + "luksUuid" : "e63581ba-79fb-4226-b9de-1888393f7573", + "luksVolumeKeySize" : 32, + "partitionUuid" : "41f9ce04-c827-4b74-a981-c669f93eb4dc", + "storage" : "luks", + "uid" : 60232 + } + }, + "disposition" : "regular", + "enforcePasswordPolicy" : false, + "lastChangeUSec" : 1565950024279735, + "memberOf" : [ + "wheel" + ], + "privileged" : { + "hashedPassword" : [ + "$6$WHBKvAFFT9jKPA4k$OPY4D4TczKN/jOnJzy54DDuOOagCcvxxybrwMbe1SVdm.Bbr.zOmBdATp.QrwZmvqyr8/SafbbQu.QZ2rRvDs/" + ] + }, + "signature" : [ + { + "data" : "LU/HeVrPZSzi3MJ0PVHwD5m/xf51XDYCrSpbDRNBdtF4fDVhrN0t2I2OqH/1yXiBidXlV0ptMuQVq8KVICdEDw==", + "key" : "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA/QT6kQWOAMhDJf56jBmszEQQpJHqDsGDMZOdiptBgRk=\n-----END PUBLIC KEY-----\n" + } + ], + "userName" : "grobie", + "status" : { + "15e19cf24e004b949ddaac60c74aa165" : { + "goodAuthenticationCounter" : 16, + "lastGoodAuthenticationUSec" : 1566309343044322, + "rateLimitBeginUSec" : 1566309342340723, + "rateLimitCount" : 1, + "state" : "inactive", + "service" : "io.systemd.Home", + "diskSize" : 161118667776, + "diskCeiling" : 190371729408, + "diskFloor" : 5242880, + "signedLocally" : true + } + } +} +``` + +When `systemd-homed.service` manages a home directory it will also include a +version of the user record in the home directory itself in the `~/.identity` +file. This version lacks the `binding` and `status` sections which are used for +local management of the user, but are not intended to be portable between +systems. It would hence look like this: + +```json +{ + "autoLogin" : true, + "disposition" : "regular", + "enforcePasswordPolicy" : false, + "lastChangeUSec" : 1565950024279735, + "memberOf" : [ + "wheel" + ], + "privileged" : { + "hashedPassword" : [ + "$6$WHBKvAFFT9jKPA4k$OPY4D4TczKN/jOnJzy54DDuOOagCcvxxybrwMbe1SVdm.Bbr.zOmBdATp.QrwZmvqyr8/SafbbQu.QZ2rRvDs/" + ] + }, + "signature" : [ + { + "data" : "LU/HeVrPZSzi3MJ0PVHwD5m/xf51XDYCrSpbDRNBdtF4fDVhrN0t2I2OqH/1yXiBidXlV0ptMuQVq8KVICdEDw==", + "key" : "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA/QT6kQWOAMhDJf56jBmszEQQpJHqDsGDMZOdiptBgRk=\n-----END PUBLIC KEY-----\n" + } + ], + "userName" : "grobie", +} +``` From 32eb3c42299d70da6214ab89d9e239d9a4808685 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 Nov 2019 21:51:55 +0100 Subject: [PATCH 28/29] docs: add documentation for JSON group records, too --- docs/GROUP_RECORD.md | 158 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 docs/GROUP_RECORD.md diff --git a/docs/GROUP_RECORD.md b/docs/GROUP_RECORD.md new file mode 100644 index 0000000000..8f98b49c27 --- /dev/null +++ b/docs/GROUP_RECORD.md @@ -0,0 +1,158 @@ +--- +title: JSON Group Records +category: Interfaces +layout: default +--- + +# JSON Group Records + +Long story short: JSON Group Records are to `struct group` what [JSON User +Records](https://systemd.io/USER_RECORD.md) are to `struct passwd`. + +Conceptually, much of what applies to JSON user records also applies to JSON +group records. They also consist of seven sections, with similar properties and +they carry some identical (or at least very similar) fields. + +## Fields in the `regular` section + +`groupName` → A string with the UNIX group name. Matches the `gr_name` field of +UNIX/glibc NSS `struct group`, or the shadow structure `struct sgrp`'s +`sg_namp` field. + +`realm` → The "realm" the group belongs to, conceptually identical to the same +field of user records. A string in DNS domain name syntax. + +`disposition` → The disposition of the group, conceptually identical to the +same field of user records. A string. + +`service` → A string, an identifier for the service managing this group record +(this field is typically in reverse domain name syntax.) + +`lastChangeUSec` → An unsigned 64bit integer, a timestamp (in µs since the UNIX +epoch 1970) of the last time the group record has been modified. (Covers only +the `regular`, `perMachine` and `privileged` sections). + +`gid` → An unsigned integer in the range 0…4294967295: the numeric UNIX group +ID (GID) to use for the group. This corresponds to the `gr_gid` field of +`struct group`. + +`members` → An array of strings, listing user names that are members of this +group. Note that JSON user records also contain a `memberOf` field, or in other +words a group membership can either be denoted in the JSON user record or in +the JSON group record, or in both. The list of memberships should be determined +as the combination of both lists (plus optionally others). If a user is listed +as member of a group and doesn't exist it should be ignored. This field +corresponds to the `gr_mem` field of `struct group` and the `sg_mem` field of +`struct sgrp`. + +`administrators` → Similarly, an array of strings, listing user names that +shall be considered "administrators" of this group. This field corresponds to +the `sg_adm` field of `struct sgrp`. + +`privileged`/`perMachine`/`binding`/`status`/`signature`/`secret` → The +objects/arrays for the other six group record sections. These are organized the +same way as for the JSON user records, and have the same semantics. + +## Fields in the `privileged` section + +The following fields are defined: + +`hashedPassword` → An array of strings with UNIX hashed passwords; see the +matching field for user records for details. This field corresponds to the +`sg_passwd` field of `struct sgrp` (and `gr_passwd` of `struct group` in a +way). + +## Fields in the `perMachine` section + +`matchMachineId`/`matchHostname` → Strings, match expressions similar as for +user records, see the user record documentation for details. + +The following fields are defined for the `perMachine` section and are defined +equivalent to the fields of the same name in the `regular` section, and +override those: + +`gid`, `members`, `administrators` + +## Fields in the `binding` section + +The following fields are defined for the `binding` section, and are equivalent +to the fields of the same name in the `regular` and `perMachine` sections: + +`gid` + +## Fields in the `status` section + +The following fields are defined in the `status` section, and are mostly +equivalent to the fields of the same name in the `regular` section, though with +slightly different conceptual semantics, see the same fields in the user record +documentation: + +`service` + +## Fields in the `signature` section + +The fields in this section are defined identically to those in the matching +section in the user record. + +## Fields in the `secret` section + +Currently no fields are defined in this section for group records. + +## Mapping to `struct group` and `struct sgrp` + +When mapping classic UNIX group records (i.e. `struct group` and `struct sgrp`) +to JSON group records the following mappings should be applied: + +| Structure | Field | Section | Field | Condition | +|----------------|-------------|--------------|------------------|----------------------------| +| `struct group` | `gr_name` | `regular` | `groupName` | | +| `struct group` | `gr_passwd` | `privileged` | `password` | (See notes below) | +| `struct group` | `gr_gid` | `regular` | `gid` | | +| `struct group` | `gr_mem` | `regular` | `members` | | +| `struct sgrp` | `sg_namp` | `regular` | `groupName` | | +| `struct sgrp` | `sg_passwd` | `privileged` | `password` | (See notes below) | +| `struct sgrp` | `sg_adm` | `regular` | `administrators` | | +| `struct sgrp` | `sg_mem` | `regular` | `members` | | + +At this time almost all Linux machines employ shadow passwords, thus the +`gr_passwd` field in `struct group` is set to `"x"`, and the actual password +is stored in the shadow entry `struct sgrp`'s field `sg_passwd`. + +## Extending These Records + +The same logic and recommendations apply as for JSON user records. + +## Examples + +A reasonable group record for a system group might look like this: + +```json +{ + "groupName" : "systemd-resolve", + "gid" : 193, + "status" : { + "6b18704270e94aa896b003b4340978f1" : { + "service" : "io.systemd.NameServiceSwitch" + } + } +} +``` + +And here's a more complete one for a regular group: + +```json +{ + "groupName" : "grobie", + "binding" : { + "6b18704270e94aa896b003b4340978f1" : { + "gid" : 60232 + } + }, + "disposition" : "regular", + "status" : { + "6b18704270e94aa896b003b4340978f1" : { + "service" : "io.systemd.Home" + } + } +} +``` From c903ee897681b016db78fb45ea72c8387f1a25cf Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 28 Aug 2019 19:36:01 +0200 Subject: [PATCH 29/29] docs: add documentation for the varlink user/group APIs --- docs/USER_GROUP_API.md | 267 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 docs/USER_GROUP_API.md diff --git a/docs/USER_GROUP_API.md b/docs/USER_GROUP_API.md new file mode 100644 index 0000000000..21d498f5fd --- /dev/null +++ b/docs/USER_GROUP_API.md @@ -0,0 +1,267 @@ +--- +title: User/Group Record Lookup API via Varlink +category: Interfaces +layout: default +--- + +# User/Group Record Lookup API via Varlink + +JSON User/Group Records (as described in the [JSON User +Records](https://systemd.io/USER_RECORD) and [JSON Group +Records](https://systemd.io/GROUP_RECORD) documents) that are defined on the +local system may be queried with a [Varlink](https://varlink.org/) API. This +API takes both the role of what +[`getpwnam(3)`](http://man7.org/linux/man-pages/man3/getpwnam.3.html) and +related calls are for `struct passwd`, as well as the interfaces modules +implementing the [glibc Name Service Switch +(NSS)](https://www.gnu.org/software/libc/manual/html_node/Name-Service-Switch.html) +expose. Or in other words, it both allows applications to efficiently query +user/group records from local services, and allows local subsystems to provide +user/group records efficiently to local applications. + +This simple API only exposes only three method calls, and requires only a small +subset of the Varlink functionality. + +## Why Varlink? + +The API described in this document is based on a simple subset of the +mechanisms described by [Varlink](https://varlink.org/). The choice of +preferring Varlink over D-Bus and other IPCs in this context was made for three +reasons: + +1. User/Group record resolution should work during early boot and late shutdown + without special handling. This is very hard to do with D-Bus, as the broker + service for D-Bus generally runs as regular system daemon and is hence only + available at the latest boot stage. + +2. The JSON user/group records are native JSON data, hence picking an IPC + system that natively operates with JSON data is natural and clean. + +3. IPC systems such as D-Bus do not provide flow control and are thus unusable + for streaming data. They are useful to pass around short control messages, + but as soon as potentially many and large objects shall be transferred, + D-Bus is not suitable, as any such streaming of messages would be considered + flooding in D-Bus' logic, and thus possibly result in termination of + communication. Since the APIs defined in this document need to support + enumerating potentially large numbers of users and groups, D-Bus is simply + not an appropriate option. + +## Concepts + +Each subsystem that needs to define users and groups on the local system is +supposed to implement this API, and offer its interfaces on a Varlink +`AF_UNIX`/`SOCK_STREAM` file system socket bound into the +`/run/systemd/userdb/` directory. When a client wants to look up a user or +group record, it contacts all sockets bound in this directory in parallel, and +enqueues the same query to each. The first positive reply is then returned to +the application, or if all fail the last seen error is returned +instead. (Alternatively a special Varlink service is available, +`io.systemd.Multiplexer` which acts as frontend and will do the parallel +queries on behalf of the client, drastically simplifying client +development. This service is not available during earliest boot and final +shutdown phases.) + +Unlike with glibc NSS there's no order or programmatic expression language +defined in which queries are issued to the various services. Instead, all +queries are always enqueued in parallel to all defined services, in order to +make look-ups efficient, and the simple rule of "first successful lookup wins" +is unconditionally followed for user and group look-ups (though not for +membership lookups, see below). + +This simple scheme only works safely as long as every service providing +user/group records carefully makes sure not to answer with conflicting +records. This API does not define any mechanisms for dealing with user/group +name/ID collisions during look-up nor during record registration. It assumes +the various subsystems that want to offer user and group records to the rest of +the system have made sufficiently sure in advance that their definitions do not +collide with those of other services. Clients are not expected to merge +multiple definitions for the same user or group, and will also not be able to +detect conflicts and suppress such conflicting records. + +It is recommended to name the sockets in the directory in reverse domain name +notation, but this is neither required nor enforced. + +## Well-Known Services + +Any subsystem that wants to provide user/group records can do so, simply by +binding a socket in the aforementioned directory. By default two +services are listening there, that have special relevance: + +1. `io.systemd.NameServiceSwitch` → This service makes the classic UNIX/glibc + NSS user/group records available as JSON User/Group records. Any such + records are automatically converted as needed, and possibly augmented with + information from the shadow databases. + +2. `io.systemd.Multiplexer` → This service multiplexes client queries to all + other running services. It's supposed to simplify client development: in + order to look up or enumerate user/group records it's sufficient to talk to + one service instead of all of them in parallel. Note that it is not availabe + during earliest boot and final shutdown phases, hence for programs running + in that context it is preferable to implement the parallel lookup + themselves. + +Both these services are implemented by the same daemon +`systemd-userdbd.service`. + +Note that these services currently implement a subset of Varlink only. For +example, introspection is not available, and the resolver logic is not used. + +## Other Services + +The `systemd` project provides two other services implementing this +interface. Specifically: + +1. `io.systemd.DynamicUser` → This service is implemented by the service + manager itself, and provides records for the users and groups synthesized + via `DynamicUser=` in unit files. + +2. `io.systemd.Home` → This service is implemented by `systemd-homed.service` + and provides records for the users and groups defined by the home + directories it manages. + +Other projects are invited to implement these services too. For example it +would make sense for LDAP/ActiveDirectory projects to implement these +interfaces, which would provide them a way to do per-user resource management +enforced by systemd and defined directly in LDAP directories. + +## Compatibility with NSS + +Two-way compatibility with classic UNIX/glibc NSS user/group records is +provided. When using the Varlink API, lookups into databases provided only via +NSS (and not natively via Varlink) are handled by the +`io.systemd.NameServiceSwitch` service (see above). When using the NSS API +(i.e. `getpwnam()` and friends) the `nss-systemd` module will automatically +synthesize NSS records for users/groups natively defined via a Varlink +API. Special care is taken to avoid recursion between these two compatibility +mechanisms. + +Subsystems that shall provide user/group records to the system may choose +between offering them via an NSS module or via a this Varlink API, either way +all records are accessible via both APIs, due to the bidirectional +forwarding. It is also possible to provide the same records via both APIs +directly, but in that case the compatibility logic must be turned off. There +are mechanisms in place for this, please contact the systemd project for +details, as these are currently not documented. + +## Caching of User Records + +This API defines no concepts for caching records. If caching is desired it +should be implemented in the subsystems that provide the user records, not in +the clients consuming them. + +## Method Calls + +``` +interface io.systemd.UserDatabase + +method GetUserRecord( + uid : ?int, + userName : ?string, + service : string +) -> ( + record : object, + incomplete : boolean +) + +method GetGroupRecord( + gid : ?int, + groupName : ?string, + service : string +) -> ( + record : object, + incomplete : boolean +) + +method GetMemberships( + userName : ?string, + groupName : ?string, + service : string +) -> ( + userName : string, + groupName : string +) + +error NoRecordFound() +error BadService() +error ServiceNotAvailable() +error ConflictingRecordFound() +``` + +The `GetUserRecord` method looks up or enumerates a user record. If the `uid` +parameter is set it specifies the numeric UNIX UID to search for. If the +`userName` parameter is set it specifies the name of the user to search +for. Typically, only one of the two parameters are set, depending whether a +look-up by UID or by name is desired. However, clients may also specify both +parameters, in which case a record matching both will be returned, and if only +one exists that matches one of the two parameters but not the other an error of +`ConflictingRecordFound` is returned. If neither of the two parameters are set +the whole user database is enumerated. In this case the method call needs to be +made with `more` set, so that multiple method call replies may be generated as +effect, each carrying one user record. + +The `service` parameter is mandatory and should be set to the service name +being talked to (i.e. to the same name as the `AF_UNIX` socket path, with the +`/run/systemd/userdb/` prefix removed). This is useful to allow implementation +of multiple services on the same socket (which is used by +`systemd-userdbd.service`). + +The method call returns one or more user records, depending which type of query is +used (see above). The record is returned in the `record` field. The +`incomplete` field indicates whether the record is complete. Services providing +user record lookup should only pass the `privileged` section of user records to +clients that either match the user the record is about or to sufficiently +privileged clients, for all others the section must be removed so that no +sensitive data is leaked this way. The `incomplete` parameter should indicate +whether the record has been modified like this or not (i.e. it is `true` if a +`privileged` section existed in the user record and was removed, and `false` if +no `privileged` section existed or one existed but hasn't been removed). + +If no user record matching the specified UID or name is known the error +`NoRecordFound` is returned (this is also returned if neither UID nor name are +specified, and hence enumeration requested but the subsystem currently has no +users defined). + +If a method call with an incorrectly set `service` field is received +(i.e. either not set at all, or not to the service's own name) a `BadService` +error is generated. Finally, `ServiceNotAvailable` should be returned when the +backing subsystem is not operational for some reason and hence no information +about existence or non-existence of a record can be returned nor any user +record at all. (The `service` field is defined in order to allow implementation +of daemons that provide multiple distinct user/group services over the same +`AF_UNIX` socket: in order to correctly determine which service a client wants +to talk to the client needs to provide the name in each request.) + +The `GetGroupRecord` method call works analogously but for groups. + +The `GetMemberships` method call may be used to inquire about group +memberships. The `userName` and `groupName` arguments take what the name +suggests. If one of the two is specified all matching memberships are returned, +if neither is specified all known memberships of any user and any group are +returned. The return value is a pair of user name and group name, where the +user is a member of the group. If both arguments are specified the specified +membership will be tested for, but no others, and the pair is returned if it is +defined. Unless both arguments are specified the method call needs to be made +with `more` set, so that multiple replies can be returned (since typically +there are are multiple members per group and also multiple groups a user is +member of). As with `GetUserRecord` and `GetGroupRecord` the `service` +parameter needs to contain the name of the service being talked to, in order to +allow implementation of multiple service within the same IPC socket. In case no +matching membership is known `NoRecordFound` is returned. The other two errors +are also generated in the same cases as for `GetUserRecord` and +`GetGroupRecord`. + +Unlike with `GetUserRecord` and `GetGroupRecord` the lists of memberships +returned by services are always combined. Thus unlike the other two calls a +membership lookup query has to wait for the last simultaneous query to complete +before the complete list is acquired. + +Note that only the `GetMemberships` call is authoritative about memberships of +users in groups. i.e. it should not be considered sufficient to check the +`memberOf` field of user records and the `members` field of group records to +acquire the full list of memberships. The full list can only bet determined by +`GetMemberships`, and as mentioned requires merging of these lists of all local +services. Result of this is that it can be one service that defines a user A, +and another service that defines a group B, and a third service that declares +that A is a member of B. + +And that's really all there is to it.