1
0
mirror of https://github.com/systemd/systemd.git synced 2024-10-26 08:55:40 +03:00

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
commit 210fb8626f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 401 additions and 62 deletions

View File

@ -174,6 +174,75 @@
<xi:include href="version-info.xml" xpointer="v250"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--fuzzy</option></term>
<term><option>-z</option></term>
<listitem><para>When used with the <command>user</command> or <command>group</command> command, do a
fuzzy string search. Any specified arguments will be matched against the user name, the real name of
the user record, the email address, and other descriptive strings of the user or group
record. Moreover, instead of precise matching, a substring match or a match allowing slight
deviations in spelling is applied.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--disposition=</option></term>
<listitem><para>When used with the <command>user</command> or <command>group</command> command,
filters by disposition of the record. Takes one of <literal>intrinsic</literal>,
<literal>system</literal>, <literal>regular</literal>, <literal>dynamic</literal>,
<literal>container</literal>. May be used multiple times, in which case only users matching any of
the specified dispositions are shown.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><option>-I</option></term>
<term><option>-S</option></term>
<term><option>-R</option></term>
<listitem><para>Shortcuts for <option>--disposition=intrinsic</option>,
<option>--disposition=system</option>, <option>--disposition=regular</option>,
respectively.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--uid-min=</option></term>
<term><option>--uid-max=</option></term>
<listitem><para>When used with the <command>user</command> or <command>group</command> command,
filters the output by UID/GID ranges. Takes numeric minimum resp. maximum UID/GID values. Shows only
records within the specified range. When applied to the <command>user</command> command matches
against UIDs, when applied to the <command>group</command> command against GIDs (despite the name of
the switch). If unspecified defaults to 0 (for the minimum) and 4294967294 (for the maximum), i.e. by
default no filtering is applied as the whole UID/GID range is covered.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--boundaries=</option></term>
<listitem><para>When used with the <command>user</command> or <command>group</command> command,
controls whether to show relevant UID/GID range boundary information in the tabular output. Takes a
boolean. Defaults to true.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><option>-B</option></term>
<listitem><para>Shortcut for <option>--boundaries=no</option>.</para>
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<xi:include href="standard-options.xml" xpointer="no-pager" />
<xi:include href="standard-options.xml" xpointer="no-legend" />
<xi:include href="standard-options.xml" xpointer="help" />

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;
}

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);

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",

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_;

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,8 +459,10 @@ static int display_user(int argc, char *argv[], void *userdata) {
}
if (table) {
int boundary_lines = 0, uid_map_lines = 0;
if (arg_boundaries) {
_cleanup_(uid_range_freep) UIDRange *uid_range = NULL;
int boundary_lines, uid_map_lines;
r = uid_range_load_userns(/* path = */ NULL, UID_RANGE_USERNS_INSIDE, &uid_range);
if (r < 0)
@ -439,6 +475,7 @@ static int display_user(int argc, char *argv[], void *userdata) {
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,9 +787,10 @@ 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;
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");
@ -742,6 +802,7 @@ static int display_group(int argc, char *argv[], void *userdata) {
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;
}

View File

@ -426,6 +426,13 @@ userdbctl -j --json=short | jq
userdbctl --with-varlink=no
userdbctl user
userdbctl user -S
userdbctl user -IS
userdbctl user -R
userdbctl user --disposition=regular --disposition=intrinsic
userdbctl user kkkk -z
userdbctl user --uid-min=100 --uid-max=100
userdbctl user -B
userdbctl user testuser
userdbctl user root
userdbctl user testuser root
@ -448,6 +455,13 @@ userdbctl user --with-nss=no 2000000
(! userdbctl user --with-dropin=no 2000000)
userdbctl group
userdbctl group -S
userdbctl group -IS
userdbctl group -R
userdbctl group --disposition=regular --disposition=intrinsic
userdbctl group kkkk -z
userdbctl group --uid-min=100 --uid-max=100
userdbctl group -B
userdbctl group testuser
userdbctl group root
userdbctl group testuser root