Merge pull request #34875 from poettering/userdbctl-filter

userdbctl: add some basic client-side filtering
This commit is contained in:
Lennart Poettering
2024-10-24 22:36:22 +02:00
committed by GitHub
8 changed files with 401 additions and 62 deletions
+25
View File
@@ -326,3 +326,28 @@ int group_record_clone(GroupRecord *h, UserRecordLoadFlags flags, GroupRecord **
*ret = TAKE_PTR(c);
return 0;
}
int group_record_match(GroupRecord *h, const UserDBMatch *match) {
assert(h);
assert(match);
if (h->gid < match->gid_min || h->gid > match->gid_max)
return false;
if (!FLAGS_SET(match->disposition_mask, UINT64_C(1) << group_record_disposition(h)))
return false;
if (!strv_isempty(match->fuzzy_names)) {
const char* names[] = {
h->group_name,
group_record_group_name_and_realm(h),
h->description,
};
if (!user_name_fuzzy_match(names, ELEMENTSOF(names), match->fuzzy_names))
return false;
}
return true;
}
+2
View File
@@ -43,5 +43,7 @@ int group_record_load(GroupRecord *h, sd_json_variant *v, UserRecordLoadFlags fl
int group_record_build(GroupRecord **ret, ...);
int group_record_clone(GroupRecord *g, UserRecordLoadFlags flags, GroupRecord **ret);
int group_record_match(GroupRecord *h, const UserDBMatch *match);
const char* group_record_group_name_and_realm(GroupRecord *h);
UserDisposition group_record_disposition(GroupRecord *h);
+24 -24
View File
@@ -104,37 +104,37 @@ int nss_passwd_to_user_record(
* just a password instead of the whole account, but that's mostly pointless in times of
* password-less authorization, hence let's not bother. */
SET_IF(hr->locked,
spwd && spwd->sp_expire >= 0,
spwd->sp_expire <= 1, -1);
SET_IF(hr->locked,
spwd && spwd->sp_expire >= 0,
spwd->sp_expire <= 1, -1);
SET_IF(hr->not_after_usec,
spwd && spwd->sp_expire > 1 && (uint64_t) spwd->sp_expire < (UINT64_MAX-1)/USEC_PER_DAY,
spwd->sp_expire * USEC_PER_DAY, UINT64_MAX);
SET_IF(hr->not_after_usec,
spwd && spwd->sp_expire > 1 && (uint64_t) spwd->sp_expire < (UINT64_MAX-1)/USEC_PER_DAY,
spwd->sp_expire * USEC_PER_DAY, UINT64_MAX);
SET_IF(hr->password_change_now,
spwd && spwd->sp_lstchg >= 0,
spwd->sp_lstchg == 0, -1);
SET_IF(hr->password_change_now,
spwd && spwd->sp_lstchg >= 0,
spwd->sp_lstchg == 0, -1);
SET_IF(hr->last_password_change_usec,
spwd && spwd->sp_lstchg > 0 && (uint64_t) spwd->sp_lstchg <= (UINT64_MAX-1)/USEC_PER_DAY,
spwd->sp_lstchg * USEC_PER_DAY, UINT64_MAX);
SET_IF(hr->last_password_change_usec,
spwd && spwd->sp_lstchg > 0 && (uint64_t) spwd->sp_lstchg <= (UINT64_MAX-1)/USEC_PER_DAY,
spwd->sp_lstchg * USEC_PER_DAY, UINT64_MAX);
SET_IF(hr->password_change_min_usec,
spwd && spwd->sp_min > 0 && (uint64_t) spwd->sp_min <= (UINT64_MAX-1)/USEC_PER_DAY,
spwd->sp_min * USEC_PER_DAY, UINT64_MAX);
SET_IF(hr->password_change_min_usec,
spwd && spwd->sp_min > 0 && (uint64_t) spwd->sp_min <= (UINT64_MAX-1)/USEC_PER_DAY,
spwd->sp_min * USEC_PER_DAY, UINT64_MAX);
SET_IF(hr->password_change_max_usec,
spwd && spwd->sp_max > 0 && (uint64_t) spwd->sp_max <= (UINT64_MAX-1)/USEC_PER_DAY,
spwd->sp_max * USEC_PER_DAY, UINT64_MAX);
SET_IF(hr->password_change_max_usec,
spwd && spwd->sp_max > 0 && (uint64_t) spwd->sp_max <= (UINT64_MAX-1)/USEC_PER_DAY,
spwd->sp_max * USEC_PER_DAY, UINT64_MAX);
SET_IF(hr->password_change_warn_usec,
spwd && spwd->sp_warn > 0 && (uint64_t) spwd->sp_warn <= (UINT64_MAX-1)/USEC_PER_DAY,
spwd->sp_warn * USEC_PER_DAY, UINT64_MAX);
SET_IF(hr->password_change_warn_usec,
spwd && spwd->sp_warn > 0 && (uint64_t) spwd->sp_warn <= (UINT64_MAX-1)/USEC_PER_DAY,
spwd->sp_warn * USEC_PER_DAY, UINT64_MAX);
SET_IF(hr->password_change_inactive_usec,
spwd && spwd->sp_inact > 0 && (uint64_t) spwd->sp_inact <= (UINT64_MAX-1)/USEC_PER_DAY,
spwd->sp_inact * USEC_PER_DAY, UINT64_MAX);
SET_IF(hr->password_change_inactive_usec,
spwd && spwd->sp_inact > 0 && (uint64_t) spwd->sp_inact <= (UINT64_MAX-1)/USEC_PER_DAY,
spwd->sp_inact * USEC_PER_DAY, UINT64_MAX);
hr->json = sd_json_variant_unref(hr->json);
r = sd_json_buildo(
+66
View File
@@ -2401,6 +2401,72 @@ int suitable_blob_filename(const char *name) {
name[0] != '.';
}
bool user_name_fuzzy_match(const char *names[], size_t n_names, char **matches) {
assert(names || n_names == 0);
/* Checks if any of the user record strings in the names[] array matches any of the search strings in
* the matches** strv fuzzily. */
FOREACH_ARRAY(n, names, n_names) {
if (!*n)
continue;
_cleanup_free_ char *lcn = strdup(*n);
if (!lcn)
return -ENOMEM;
ascii_strlower(lcn);
STRV_FOREACH(i, matches) {
_cleanup_free_ char *lc = strdup(*i);
if (!lc)
return -ENOMEM;
ascii_strlower(lc);
/* First do substring check */
if (strstr(lcn, lc))
return true;
/* Then do some fuzzy string comparison (but only if the needle is non-trivially long) */
if (strlen(lc) >= 5 && strlevenshtein(lcn, lc) < 3)
return true;
}
}
return false;
}
int user_record_match(UserRecord *u, const UserDBMatch *match) {
assert(u);
assert(match);
if (u->uid < match->uid_min || u->uid > match->uid_max)
return false;
if (!FLAGS_SET(match->disposition_mask, UINT64_C(1) << user_record_disposition(u)))
return false;
if (!strv_isempty(match->fuzzy_names)) {
/* Note this array of names is sparse, i.e. various entries listed in it will be
* NULL. Because of that we are not using a NULL terminated strv here, but a regular
* array. */
const char* names[] = {
u->user_name,
user_record_user_name_and_realm(u),
u->real_name,
u->email_address,
u->cifs_user_name,
};
if (!user_name_fuzzy_match(names, ELEMENTSOF(names), match->fuzzy_names))
return false;
}
return true;
}
static const char* const user_storage_table[_USER_STORAGE_MAX] = {
[USER_CLASSIC] = "classic",
[USER_LUKS] = "luks",
+18
View File
@@ -462,6 +462,24 @@ int user_group_record_mangle(sd_json_variant *v, UserRecordLoadFlags load_flags,
#define BLOB_DIR_MAX_SIZE (UINT64_C(64) * U64_MB)
int suitable_blob_filename(const char *name);
typedef struct UserDBMatch {
char **fuzzy_names;
uint64_t disposition_mask;
union {
uid_t uid_min;
gid_t gid_min;
};
union {
uid_t uid_max;
gid_t gid_max;
};
} UserDBMatch;
#define USER_DISPOSITION_MASK_MAX ((UINT64_C(1) << _USER_DISPOSITION_MAX) - UINT64_C(1))
bool user_name_fuzzy_match(const char *names[], size_t n_names, char **matches);
int user_record_match(UserRecord *u, const UserDBMatch *match);
const char* user_storage_to_string(UserStorage t) _const_;
UserStorage user_storage_from_string(const char *s) _pure_;
+183 -38
View File
@@ -37,6 +37,11 @@ static char** arg_services = NULL;
static UserDBFlags arg_userdb_flags = 0;
static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF;
static bool arg_chain = false;
static uint64_t arg_disposition_mask = UINT64_MAX;
static uid_t arg_uid_min = 0;
static uid_t arg_uid_max = UID_INVALID-1;
static bool arg_fuzzy = false;
static bool arg_boundaries = true;
STATIC_DESTRUCTOR_REGISTER(arg_services, strv_freep);
@@ -63,6 +68,10 @@ static const char *user_disposition_to_color(UserDisposition d) {
}
}
static const char* shell_to_color(const char *shell) {
return !shell || is_nologin_shell(shell) ? ansi_grey() : NULL;
}
static int show_user(UserRecord *ur, Table *table) {
int r;
@@ -99,10 +108,9 @@ static int show_user(UserRecord *ur, Table *table) {
break;
case OUTPUT_TABLE: {
UserDisposition d;
assert(table);
d = user_record_disposition(ur);
UserDisposition d = user_record_disposition(ur);
const char *sh = user_record_shell(ur);
r = table_add_many(
table,
@@ -113,8 +121,9 @@ static int show_user(UserRecord *ur, Table *table) {
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_PATH, user_record_home_directory(ur),
TABLE_PATH, sh,
TABLE_SET_COLOR, shell_to_color(sh),
TABLE_INT, 0);
if (r < 0)
return table_log_add_error(r);
@@ -176,6 +185,9 @@ static int table_add_uid_boundaries(Table *table, const UIDRange *p) {
FOREACH_ELEMENT(i, uid_range_table) {
_cleanup_free_ char *name = NULL, *comment = NULL;
if (!FLAGS_SET(arg_disposition_mask, UINT64_C(1) << i->disposition))
continue;
if (!uid_range_covers(p, i->first, i->last - i->first + 1))
continue;
@@ -346,7 +358,7 @@ static int display_user(int argc, char *argv[], void *userdata) {
int ret = 0, r;
if (arg_output < 0)
arg_output = argc > 1 ? OUTPUT_FRIENDLY : OUTPUT_TABLE;
arg_output = argc > 1 && !arg_fuzzy ? OUTPUT_FRIENDLY : OUTPUT_TABLE;
if (arg_output == OUTPUT_TABLE) {
table = table_new(" ", "name", "disposition", "uid", "gid", "realname", "home", "shell", "order");
@@ -357,10 +369,18 @@ static int display_user(int argc, char *argv[], void *userdata) {
(void) table_set_align_percent(table, table_get_cell(table, 0, 4), 100);
table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
(void) table_set_sort(table, (size_t) 3, (size_t) 8);
(void) table_set_display(table, (size_t) 0, (size_t) 1, (size_t) 2, (size_t) 3, (size_t) 4, (size_t) 5, (size_t) 6, (size_t) 7);
(void) table_hide_column_from_display(table, (size_t) 8);
if (!arg_boundaries)
(void) table_hide_column_from_display(table, (size_t) 0);
}
if (argc > 1)
UserDBMatch match = {
.disposition_mask = arg_disposition_mask,
.uid_min = arg_uid_min,
.uid_max = arg_uid_max,
};
if (argc > 1 && !arg_fuzzy)
STRV_FOREACH(i, argv + 1) {
_cleanup_(user_record_unrefp) UserRecord *ur = NULL;
uid_t uid;
@@ -377,8 +397,10 @@ static int display_user(int argc, char *argv[], void *userdata) {
else
log_error_errno(r, "Failed to find user %s: %m", *i);
if (ret >= 0)
ret = r;
RET_GATHER(ret, r);
} else if (!user_record_match(ur, &match)) {
log_error("User '%s' does not match filter.", *i);
RET_GATHER(ret, -ENOEXEC);
} else {
if (draw_separator && arg_output == OUTPUT_FRIENDLY)
putchar('\n');
@@ -392,6 +414,15 @@ static int display_user(int argc, char *argv[], void *userdata) {
}
else {
_cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
_cleanup_strv_free_ char **names = NULL;
if (argc > 1) {
names = strv_copy(argv + 1);
if (!names)
return log_oom();
match.fuzzy_names = names;
}
r = userdb_all(arg_userdb_flags, &iterator);
if (r == -ENOLINK) /* ENOLINK → Didn't find answer without Varlink, and didn't try Varlink because was configured to off. */
@@ -412,6 +443,9 @@ static int display_user(int argc, char *argv[], void *userdata) {
if (r < 0)
return log_error_errno(r, "Failed acquire next user: %m");
if (!user_record_match(ur, &match))
continue;
if (draw_separator && arg_output == OUTPUT_FRIENDLY)
putchar('\n');
@@ -425,20 +459,23 @@ static int display_user(int argc, char *argv[], void *userdata) {
}
if (table) {
_cleanup_(uid_range_freep) UIDRange *uid_range = NULL;
int boundary_lines, uid_map_lines;
int boundary_lines = 0, uid_map_lines = 0;
r = uid_range_load_userns(/* path = */ NULL, UID_RANGE_USERNS_INSIDE, &uid_range);
if (r < 0)
log_debug_errno(r, "Failed to load /proc/self/uid_map, ignoring: %m");
if (arg_boundaries) {
_cleanup_(uid_range_freep) UIDRange *uid_range = NULL;
boundary_lines = table_add_uid_boundaries(table, uid_range);
if (boundary_lines < 0)
return boundary_lines;
r = uid_range_load_userns(/* path = */ NULL, UID_RANGE_USERNS_INSIDE, &uid_range);
if (r < 0)
log_debug_errno(r, "Failed to load /proc/self/uid_map, ignoring: %m");
uid_map_lines = table_add_uid_map(table, uid_range, add_unavailable_uid);
if (uid_map_lines < 0)
return uid_map_lines;
boundary_lines = table_add_uid_boundaries(table, uid_range);
if (boundary_lines < 0)
return boundary_lines;
uid_map_lines = table_add_uid_map(table, uid_range, add_unavailable_uid);
if (uid_map_lines < 0)
return uid_map_lines;
}
if (!table_isempty(table)) {
r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
@@ -650,7 +687,7 @@ static int display_group(int argc, char *argv[], void *userdata) {
int ret = 0, r;
if (arg_output < 0)
arg_output = argc > 1 ? OUTPUT_FRIENDLY : OUTPUT_TABLE;
arg_output = argc > 1 && !arg_fuzzy ? OUTPUT_FRIENDLY : OUTPUT_TABLE;
if (arg_output == OUTPUT_TABLE) {
table = table_new(" ", "name", "disposition", "gid", "description", "order");
@@ -660,10 +697,18 @@ static int display_group(int argc, char *argv[], void *userdata) {
(void) table_set_align_percent(table, table_get_cell(table, 0, 3), 100);
table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
(void) table_set_sort(table, (size_t) 3, (size_t) 5);
(void) table_set_display(table, (size_t) 0, (size_t) 1, (size_t) 2, (size_t) 3, (size_t) 4);
(void) table_hide_column_from_display(table, (size_t) 5);
if (!arg_boundaries)
(void) table_hide_column_from_display(table, (size_t) 0);
}
if (argc > 1)
UserDBMatch match = {
.disposition_mask = arg_disposition_mask,
.gid_min = arg_uid_min,
.gid_max = arg_uid_max,
};
if (argc > 1 && !arg_fuzzy)
STRV_FOREACH(i, argv + 1) {
_cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
gid_t gid;
@@ -680,8 +725,10 @@ static int display_group(int argc, char *argv[], void *userdata) {
else
log_error_errno(r, "Failed to find group %s: %m", *i);
if (ret >= 0)
ret = r;
RET_GATHER(ret, r);
} else if (!group_record_match(gr, &match)) {
log_error("Group '%s' does not match filter.", *i);
RET_GATHER(ret, -ENOEXEC);
} else {
if (draw_separator && arg_output == OUTPUT_FRIENDLY)
putchar('\n');
@@ -695,6 +742,15 @@ static int display_group(int argc, char *argv[], void *userdata) {
}
else {
_cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
_cleanup_strv_free_ char **names = NULL;
if (argc > 1) {
names = strv_copy(argv + 1);
if (!names)
return log_oom();
match.fuzzy_names = names;
}
r = groupdb_all(arg_userdb_flags, &iterator);
if (r == -ENOLINK)
@@ -715,6 +771,9 @@ static int display_group(int argc, char *argv[], void *userdata) {
if (r < 0)
return log_error_errno(r, "Failed acquire next group: %m");
if (!group_record_match(gr, &match))
continue;
if (draw_separator && arg_output == OUTPUT_FRIENDLY)
putchar('\n');
@@ -728,20 +787,22 @@ static int display_group(int argc, char *argv[], void *userdata) {
}
if (table) {
_cleanup_(uid_range_freep) UIDRange *gid_range = NULL;
int boundary_lines, gid_map_lines;
int boundary_lines = 0, gid_map_lines = 0;
r = uid_range_load_userns(/* path = */ NULL, GID_RANGE_USERNS_INSIDE, &gid_range);
if (r < 0)
log_debug_errno(r, "Failed to load /proc/self/gid_map, ignoring: %m");
if (arg_boundaries) {
_cleanup_(uid_range_freep) UIDRange *gid_range = NULL;
r = uid_range_load_userns(/* path = */ NULL, GID_RANGE_USERNS_INSIDE, &gid_range);
if (r < 0)
log_debug_errno(r, "Failed to load /proc/self/gid_map, ignoring: %m");
boundary_lines = table_add_gid_boundaries(table, gid_range);
if (boundary_lines < 0)
return boundary_lines;
boundary_lines = table_add_gid_boundaries(table, gid_range);
if (boundary_lines < 0)
return boundary_lines;
gid_map_lines = table_add_uid_map(table, gid_range, add_unavailable_gid);
if (gid_map_lines < 0)
return gid_map_lines;
gid_map_lines = table_add_uid_map(table, gid_range, add_unavailable_gid);
if (gid_map_lines < 0)
return gid_map_lines;
}
if (!table_isempty(table)) {
r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
@@ -1090,6 +1151,15 @@ static int help(int argc, char *argv[], void *userdata) {
" --multiplexer=BOOL Control whether to use the multiplexer\n"
" --json=pretty|short JSON output mode\n"
" --chain Chain another command\n"
" --uid-min=ID Filter by minimum UID/GID (default 0)\n"
" --uid-max=ID Filter by maximum UID/GID (default 4294967294)\n"
" -z --fuzzy Do a fuzzy name search\n"
" --disposition=VALUE Filter by disposition\n"
" -I Equivalent to --disposition=intrinsic\n"
" -S Equivalent to --disposition=system\n"
" -R Equivalent to --disposition=regular\n"
" --boundaries=BOOL Show/hide UID/GID range boundaries in output\n"
" -B Equivalent to --boundaries=no\n"
"\nSee the %s for details.\n",
program_invocation_short_name,
ansi_highlight(),
@@ -1113,6 +1183,10 @@ static int parse_argv(int argc, char *argv[]) {
ARG_MULTIPLEXER,
ARG_JSON,
ARG_CHAIN,
ARG_UID_MIN,
ARG_UID_MAX,
ARG_DISPOSITION,
ARG_BOUNDARIES,
};
static const struct option options[] = {
@@ -1129,6 +1203,11 @@ static int parse_argv(int argc, char *argv[]) {
{ "multiplexer", required_argument, NULL, ARG_MULTIPLEXER },
{ "json", required_argument, NULL, ARG_JSON },
{ "chain", no_argument, NULL, ARG_CHAIN },
{ "uid-min", required_argument, NULL, ARG_UID_MIN },
{ "uid-max", required_argument, NULL, ARG_UID_MAX },
{ "fuzzy", required_argument, NULL, 'z' },
{ "disposition", required_argument, NULL, ARG_DISPOSITION },
{ "boundaries", required_argument, NULL, ARG_BOUNDARIES },
{}
};
@@ -1159,7 +1238,7 @@ static int parse_argv(int argc, char *argv[]) {
int c;
c = getopt_long(argc, argv,
arg_chain ? "+hjs:N" : "hjs:N", /* When --chain was used disable parsing of further switches */
arg_chain ? "+hjs:NISRzB" : "hjs:NISRzB", /* When --chain was used disable parsing of further switches */
options, NULL);
if (c < 0)
break;
@@ -1275,6 +1354,65 @@ static int parse_argv(int argc, char *argv[]) {
arg_chain = true;
break;
case ARG_DISPOSITION: {
UserDisposition d = user_disposition_from_string(optarg);
if (d < 0)
return log_error_errno(d, "Unknown user disposition: %s", optarg);
if (arg_disposition_mask == UINT64_MAX)
arg_disposition_mask = 0;
arg_disposition_mask |= UINT64_C(1) << d;
break;
}
case 'I':
if (arg_disposition_mask == UINT64_MAX)
arg_disposition_mask = 0;
arg_disposition_mask |= UINT64_C(1) << USER_INTRINSIC;
break;
case 'S':
if (arg_disposition_mask == UINT64_MAX)
arg_disposition_mask = 0;
arg_disposition_mask |= UINT64_C(1) << USER_SYSTEM;
break;
case 'R':
if (arg_disposition_mask == UINT64_MAX)
arg_disposition_mask = 0;
arg_disposition_mask |= UINT64_C(1) << USER_REGULAR;
break;
case ARG_UID_MIN:
r = parse_uid(optarg, &arg_uid_min);
if (r < 0)
return log_error_errno(r, "Failed to parse --uid-min= value: %s", optarg);
break;
case ARG_UID_MAX:
r = parse_uid(optarg, &arg_uid_max);
if (r < 0)
return log_error_errno(r, "Failed to parse --uid-max= value: %s", optarg);
break;
case 'z':
arg_fuzzy = true;
break;
case ARG_BOUNDARIES:
r = parse_boolean_argument("boundaries", optarg, &arg_boundaries);
if (r < 0)
return r;
break;
case 'B':
arg_boundaries = false;
break;
case '?':
return -EINVAL;
@@ -1283,6 +1421,13 @@ static int parse_argv(int argc, char *argv[]) {
}
}
if (arg_uid_min > arg_uid_max)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Minimum UID/GID " UID_FMT " is above maximum UID/GID " UID_FMT ", refusing.", arg_uid_min, arg_uid_max);
/* If not mask was specified, use the all bits on mask */
if (arg_disposition_mask == UINT64_MAX)
arg_disposition_mask = USER_DISPOSITION_MASK_MAX;
return 1;
}