Systemd/src/shared/group-record.c
Lennart Poettering 7a8867abfa user-util: rework how we validate user names
This reworks the user validation infrastructure. There are now two
modes. In regular mode we are strict and test against a strict set of
valid chars. And in "relaxed" mode we just filter out some really
obvious, dangerous stuff. i.e. strict is whitelisting what is OK, but
"relaxed" is blacklisting what is really not OK.

The idea is that we use strict mode whenver we allocate a new user
(i.e. in sysusers.d or homed), while "relaxed" mode is when we process
users registered elsewhere, (i.e. userdb, logind, …)

The requirements on user name validity vary wildly. SSSD thinks its fine
to embedd "@" for example, while the suggested NAME_REGEX field on
Debian does not even allow uppercase chars…

This effectively liberaralizes a lot what we expect from usernames.

The code that warns about questionnable user names is now optional and
only used at places such as unit file parsing, so that it doesn't show
up on every userdb query, but only when processing configuration files
that know better.

Fixes: #15149 #15090
2020-04-08 17:11:20 +02:00

347 lines
12 KiB
C

/* 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), JSON_RELAX},
{ "administrators", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, administrators), JSON_RELAX},
{},
};
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), JSON_RELAX},
{ "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), JSON_RELAX},
{ "administrators", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, administrators), JSON_RELAX},
{ "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;
}