-
Notifications
You must be signed in to change notification settings - Fork 8
/
userlist
executable file
·401 lines (365 loc) · 14 KB
/
userlist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
#! /bin/sh
# userlist (Bourne shell script) -- list one or all users, with password and expiry info
#
# Version: 1.3.2
# Copyright: (c) 2014 Alastair Irvine <[email protected]>
# Keywords: users passwd group gecos expiry
# Licence: This file is released under the GNU General Public License
#
# See help() for usage.
#
# Note: Because it accesses the shadow database, userlist should be run as root.
# Otherwise it shows a warning and a minimal listing equivalent to -u.
#
# Licence details:
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or (at
# your option) any later version.
#
# See http://www.gnu.org/licenses/gpl-2.0.html for more information.
#
# You can find the complete text of the GPLv2 in the file
# /usr/share/common-licenses/GPL-2 on Debian systems.
# Or see the file COPYING in the same directory as this program.
#
#
# TO-DO:
# + Rainbow mode, which outputs all 14 pieces of information printed
# for each user in a different colour.
# + Output phone numbers, etc.
# + research: does today have to be past the expiry date to count as expired
# + option to sort output by various criteria
# + expand @<groupname> or %<groupname> into a list of users
# + test for min_age > max_age; this means user cannot change her password
# + handle max_age == 0 (this isn't valid)
self=`basename $0`
allowed_options=s:xXdqvhcCwogu
allowed_long_options=verbose,colour,color,wrap,oneline,help,unprivileged
today=$(($(date +%s) / 86400))
# *** FUNCTIONS ***
help()
{
cat << EOT_HELP
Usage: userlist [-x|-X| -s <status>] [-q] [-v [-v [-f]] [-w]] [<username> ...]
By default, shows all users, except system users (i.e. UID < 500) and
disabled users (i.e. password crypt is equal to * (or !*); these were
usually added by a package). If specific usernames are provided, they
are always shown (i.e. -s, -x and -X don't apply).
Functionality and information is reduced if not run as root.
Options:
-u Unprivileged mode (auto. with warning if not running as root)
-x Also show accounts added by packages
-X Show all accounts including system accounts
-s <status> Show only accounts with this status (never system users)
-q Don't show a header line
-v, --verbose Show a second, indented line per user with gecos info, IDs, etc.
-vv Show phone numbers, etc.
-o, --oneline Don't wrap -v output
-g Show group info
-c, --colour Colourise output, but only if STDOUT is a terminal
-C Still colourise output even if STDOUT isn't a terminal
(which is useful if piping to less -R)
-d (debug mode) displays shadow file entries to STDERR
-h, --help Show this help
Output:
The Status column is one of: active, disabled, expired.
If appropriate, it will show a reason in parentheses:
auto means password_age > Max + Grace
admin means account has been manually expired by the sysadmin
(Note: This column is limited in unprivileged mode, and is left out unless
verbose mode is active.)
The Password column is one of: set, locked, LOCKED, BLANK, none, old.
(Or "future", which only happens if the password age is negative.)
The Remaining, Age, Warn, Min., Max., Grace (a.k.a. inactivity) columns are
all given in terms of days. See shadow(5) for more information.
Remaining (pertaining to account expiry) and Age (of password) are calculated
by userlist, relative to today.
Important info:
"LOCKED" is a blank password that is locked, which isn't dangerous because
passwd refuses to unlock an account if doing so would make it passwordless.
"none" means a specific invalid crypt, used to create the account disabled.
SSH keys (or other methods, if configured), can be used to log into
*any* account with *any* Status (except "expired") or Password state.
(This assumes that SSH obeys PAM config. "PermitEmptyPasswords no" won't
prevent logins via an SSH key.)
EOT_HELP
exit
}
# Expects a list of users in passwd(5) format on stdin
process_users_full()
{
if [ $output_header = y ] ; then
# $bold and $normal aren't defined unless $colour != no
printf "$bold$username_header_format %-10s\
%9s %s %4s %5s %5s %5s %5s$normal" \
Username Status Remaining Password Age 'Warn' 'Min.' 'Max.' 'Grace'
## .. 'Warn for' 'Min. age' 'Max. age' 'Grace period'
if [ $verbose -ge 1 ] ; then
printf "$eol$bold $passwd_header_format$normal" \
"Full name" UID GID "Login shell" "Home directory"
## # output phone numbers, etc.
## if [ $verbose -ge 2 ] ; then
## printf " \n" ...
## fi
fi
# "continue' the line or just end it depending on whether there's more info
show_groups_header
fi
while IFS=: read username passwd uid gid gecos homedir loginshell _
do
[ $debug -gt 0 ] && echo "()" >&2
if [ $include_sys = all -o $uid -ge 500 -a $uid != 65534 ] ; then
getent shadow "$username" | \
while IFS=: read username passwd_crypt change_date min_age max_age warn_days grace_days expire_date
do
[ $debug -gt 0 ] && echo "($username $passwd_crypt $change_date $min_age $max_age $warn_days $grace_days $expire_date)" >&2
# See if the account is active, i.e. unexpired
status=active # this is a default
status_shown= # will be set to $status if not set
if [ -z "$expire_date" ] ; then
remaining_days=-
else
if [ $expire_date -lt $today ] ; then
status=expired
fi
# test for special case dates
if [ $expire_date = 0 -o $expire_date = 1 ] ; then
# first part appears to be adjacent to the previous column
remaining_days=0
status=expired
status_shown="expired (admin)"
else
remaining_days=$(($expire_date - $today))
fi
fi
# If it passed the expiry check, test the shell
if [ $status = active ] ; then
if [ "$shell" = /usr/sbin/nologin -o "$shell" = /bin/false ] ; then
status=disabled
else
if [ -z "$shell" ] ; then
shell=NONE
fi
fi
fi
case "$passwd_crypt" in
'') pwstatus=BLANK ;;
!\*) pwstatus=none ;;
\*) pwstatus=none ;;
\!) pwstatus=LOCKED ;;
\!*) pwstatus=locked ;;
*) pwstatus=set ;;
esac
case "$change_date" in
'') pw_age=N/A ;;
0) pw_age=change ;;
*) pw_age=$(($today - $change_date))
# Handle the cases where the password appears to have been
# changed in the future, or has expired
if [ $pw_age -lt 0 ] ; then
pwstatus=future
elif [ -n "$max_age" ] ; then
# Note that pw_age will still be set if the password is
# locked or empty
if [ "$max_age" -ge 0 -a "$pw_age" -gt "$max_age" ] ; then
if [ $pwstatus = set ] ; then
pwstatus=old
fi
# Check for the auto-expired state
if [ -z "$grace_days" ] ; then
grace_days=0
fi
if [ $pw_age -gt $(($max_age + $grace_days)) ] ; then
status=expired
status_shown="expired (auto)"
fi
fi
else
# This is the implied meaning of a blank field
max_age=-1
fi
;;
esac
# Mark users with * for a crypt as disabled, i.e. can't log in with password
if [ $pwstatus = none ] ; then
status=disabled
fi
if [ -z "$status_shown" ] ; then
status_shown=$status
fi
# Only output if conditions match
if [ $status = "$desired_status" -o \
-z "$desired_status" -a \
\( $status != disabled -o $include_sys != none \) ] ; then
printf "$username_field_format \
$status_field_format%4s %-6s %6s %5d %5d %5d %5d" \
"$username" \
"$status_shown" "$remaining_days" "$pwstatus" "$pw_age" $warn_days $min_age $max_age $grace_days
## %8d %8d %8d %12d
if [ $verbose -ge 1 ] ; then
# Print regular info from the passwd db, but strip phone number etc.
printf "$eol $passwd_field_format" \
"${gecos%%,*}" $uid $gid "$loginshell" $homedir
## # output phone numbers, etc.
## if [ $verbose -ge 2 ] ; then
## printf "$eol " ...
## fi
fi
# "continue' the line or just end it depending on whether there's more info
show_groups $username
fi
done
fi
done
}
# Expects a list of users in passwd(5) format on stdin
process_users_minimal()
{
if [ $output_header = y ] ; then
# $bold and $normal aren't defined unless $colour != no
printf "$bold$username_header_format \
$([ $verbose -ge 1 ] && echo "$status_field_format ")\
$passwd_header_format$normal" \
Username $([ $verbose -ge 1 ] && echo Status) \
"Full name" UID GID "Login shell" "Home directory"
## # output phone numbers, etc.
## if [ $verbose -ge 2 ] ; then
## printf "$eol " ...
## fi
# "continue' the line or just end it depending on whether there's more info
show_groups_header
fi
while IFS=: read username passwd uid gid gecos homedir loginshell _
do
if [ $include_sys = all -o $uid -ge 500 -a $uid != 65534 ] ; then
# See if the account is active, i.e. unexpired
status=active # this is a default
if [ "$shell" = /usr/sbin/nologin -o "$shell" = /bin/false ] ; then
status=disabled
else
if [ -z "$shell" ] ; then
shell=NONE
fi
fi
# Print unprivileged mode output, maybe with status if requested.
# (Strip phone number etc. from regular info from the passwd db.)
# Warning: $status can only ever be one word.
# (It only appears as a printf argument under some circumstances,
# i.e. sometimes the $() output is nothing, an so can't be quoted.)
printf "$username_field_format \
$([ $verbose -ge 1 ] && echo "$status_field_format ")$passwd_field_format" \
"$username" $([ $verbose -ge 1 ] && echo $status) \
"${gecos%%,*}" $uid $gid $loginshell $homedir
## # output phone numbers, etc.
## if [ $verbose -ge 2 ] ; then
## printf "$eol " ...
## fi
# "continue' the line or just end it depending on whether there's more info
show_groups $username
fi
done
}
show_groups_header()
{
if [ $show_groups = y ] ; then
printf "$eol$bold %s$normal\n" Groups
else
echo
fi
}
# Expects a username in $1
show_groups()
{
if [ $show_groups = y ] ; then
# Output the groups, stripping off the "<username> : " prefix
local groups="$(groups $1)"
printf "$eol %s\n" "${groups#* : }"
else
echo
fi
}
# *** MAINLINE ***
# == command-line parsing ==
# -- defaults --
process_fn=process_users_full
include_sys=none
output_header=y
eol="\n"
debug=0
verbose=0
colour=no
show_groups=n
# -- option handling --
set -e
orthogonal_opts=$(getopt --shell=sh --name=$self \
--options=+$allowed_options --longoptions=$allowed_long_options -- "$@")
eval set -- "$orthogonal_opts"
set +e # getopt would have already reported the error
while [ x"$1" != x-- ] ; do
case "$1" in
-u|--unprivileged) process_fn=process_users_minimal ;;
-g) show_groups=y ;;
-s) desired_status="$2" ; shift ;;
-x) include_sys=some ;;
-X) include_sys=all ;;
-q) output_header=n ;;
-o|--oneline) eol= ;; # no need for content: next line starts with " "
-w|--wrap) eol="\n" ;; # restore default
-c|--colour|--color) colour=yes ;;
-C) colour=force ;;
-d) debug=$((debug + 1)) ;;
-v|--verbose) verbose=$((verbose + 1)) ;;
-h|--help) help ;;
-1) echo "${self}: Warning: Blah blah blah feature unsupported" >&2 ;;
esac
shift # get rid of the option (or its arg if the inner shift already got rid it)
done
shift # get rid of the "--"
# == sanity checking ==
# Check if an unprivileged user forgot the -u option
if [ $(id -u) != 0 -a $process_fn = process_users_full ] ; then
echo "${self}: Warning: Not running as root; activating unprivileged mode." >&2
process_fn=process_users_minimal
fi
# == preparation ==
if [ $colour = yes -a -t 1 -o $colour = force ] ; then
bold=$(setterm -bold on)
normal=$(setterm -bold off)
fi
# $gecos(trimmed) $uid $gid $loginshell $homedir
# (Allow longer/unlimited field lengths (e.g. for $homedir) in oneline mode.)
if [ -z "$eol" ] ; then
passwd_header_format="%-19s %5s:%-5s %-18s %-30s"
passwd_field_format="%-19.19s %5d:%-5d %-18s %-30s"
username_header_format="%-16s"
username_field_format="%-16.16s"
else
# the unrestricted field on the end lets longer homedirs wrap when necessary
passwd_header_format="%-19s %5s:%-5s %-12s %s"
passwd_field_format="%-19.19s %5d:%-5d %-12s %s"
# There's more info in unprivileged mode, so make username column narrower
# (but only in multiline mode)
if [ $process_fn = process_users_minimal ] ; then
username_header_format="%-11s"
username_field_format="%-11.11s"
else
username_header_format="%-16s"
username_field_format="%-16.16s"
fi
fi
if [ $process_fn = process_users_minimal ] ; then
status_field_format="%-8s"
else
status_field_format="%-15s"
fi
# == processing ==
# check if a username was supplied
if [ $# -gt 0 ] ; then
include_sys=all
for username ; do
getent passwd "$username"
done | $process_fn
else
getent passwd | $process_fn
fi