|
| 1 | +#!/bin/sh |
| 2 | +# |
| 3 | +# This file is licensed under the BSD-3-Clause license. |
| 4 | +# See the AUTHORS and LICENSE files for more information. |
| 5 | + |
| 6 | +############ |
| 7 | +## Variables |
| 8 | +############ |
| 9 | +readonly VERSION=0.3 |
| 10 | +readonly SCRIPT_NAME=${0##*/} |
| 11 | + |
| 12 | +# Overriding IFS is the only way to handle spaces in the files without resorting |
| 13 | +# to spawning subshells :-/ |
| 14 | +readonly NEWLINE=' |
| 15 | +' |
| 16 | +readonly ORIG_IFS=$IFS |
| 17 | +IFS=$NEWLINE |
| 18 | + |
| 19 | +INCLUDE_RANGE='' |
| 20 | +INCLUDE='' |
| 21 | +EXCLUDE='' |
| 22 | +EXCLUDE_RANGE='' |
| 23 | + |
| 24 | +readonly GROUP_FILE='/etc/group' |
| 25 | +readonly PASSWD_FILE='/etc/passwd' |
| 26 | +readonly SHADOW_FILE='/etc/shadow' |
| 27 | +FILE_TO_PARSE='' |
| 28 | +FILE_CONTENTS='' |
| 29 | +SHADOW_TO_PARSE='' |
| 30 | +SHADOW_CONTENTS='' |
| 31 | + |
| 32 | +############ |
| 33 | +## FUNCTIONS |
| 34 | +############ |
| 35 | + |
| 36 | +Help() { |
| 37 | + cat << EOF |
| 38 | +$SCRIPT_NAME v${VERSION} |
| 39 | +
|
| 40 | +Syntax: |
| 41 | +$SCRIPT_NAME [-h] [-V] | |
| 42 | + [-e ID ...] [-E ID ID] [-f filename [filename]] [-i ID ...] |
| 43 | + [-I ID ID] ... |
| 44 | +
|
| 45 | +OPTIONS: |
| 46 | + -e, --exclude ID ... = Exclude these specific ID(s) |
| 47 | + -E, --exclude-range ID ID = Exclude this range of IDs |
| 48 | + -f, --file filename [filename] = File to parse rather than system default. |
| 49 | + shdwfilter requires both passwd and shadow. |
| 50 | + -h, --help = Print this help and exit |
| 51 | + -i, --include ID ... = Include these Specific ID(s) |
| 52 | + -I, --range ID ID = Include this range of IDs |
| 53 | + -V, --version = Print the version number and exit |
| 54 | +
|
| 55 | +EOF |
| 56 | +} |
| 57 | + |
| 58 | +# return 0 if a desired ID, otherwise 1 |
| 59 | +# - individuals always win over ranges |
| 60 | +# - when in conflict, exclude wins over include |
| 61 | +# - ranges are inclusive |
| 62 | +DesiredID() { |
| 63 | + local uid=$1 |
| 64 | + # TODO: check if int? |
| 65 | + |
| 66 | + # specified IDs |
| 67 | + local i |
| 68 | + for i in $EXCLUDE; do # exclude |
| 69 | + [ $uid -eq $i ] && return 1 |
| 70 | + done |
| 71 | + for i in $INCLUDE; do # include |
| 72 | + [ $uid -eq $i ] && return 0 |
| 73 | + done |
| 74 | + |
| 75 | + # ranges |
| 76 | + for i in $EXCLUDE_RANGE; do # exclude |
| 77 | + [ $uid -ge ${i%-*} ] && [ $uid -le ${i#*-} ] && return 1 |
| 78 | + done |
| 79 | + for i in $INCLUDE_RANGE; do # include |
| 80 | + [ $uid -ge ${i%-*} ] && [ $uid -le ${i#*-} ] && return 0 |
| 81 | + done |
| 82 | + |
| 83 | + # if nothing matches, reject |
| 84 | + return 1 |
| 85 | +} |
| 86 | + |
| 87 | +# print message to stderr and exit 1 |
| 88 | +Fatal() { |
| 89 | + printf '%s\n' "$*" >&2 |
| 90 | + exit 1 |
| 91 | +} |
| 92 | + |
| 93 | +# return 0 if argument is an integer; 1 if not |
| 94 | +IsInt() { |
| 95 | + [ -z "${1##*[!0-9]*}" ] && return 1 || return 0 |
| 96 | +} |
| 97 | + |
| 98 | +####### |
| 99 | +## MAIN |
| 100 | +####### |
| 101 | + |
| 102 | +# functionality changes depending on how the script is called (typically via symlinks) |
| 103 | +case "$SCRIPT_NAME" in |
| 104 | + grpfilter*) |
| 105 | + FILE_TO_PARSE=$GROUP_FILE |
| 106 | + ;; |
| 107 | + pwfilter*) |
| 108 | + FILE_TO_PARSE=$PASSWD_FILE |
| 109 | + ;; |
| 110 | + shdwfilter*) |
| 111 | + FILE_TO_PARSE=$PASSWD_FILE |
| 112 | + SHADOW_TO_PARSE=$SHADOW_FILE |
| 113 | + ;; |
| 114 | + *) |
| 115 | + Fatal "'$SCRIPT_NAME' is an unsupported script name for the pwfilter suite." |
| 116 | + ;; |
| 117 | +esac |
| 118 | + |
| 119 | +# help out if there are no arguments |
| 120 | +[ -n "$1" ] || { Help; exit 1; } |
| 121 | + |
| 122 | +# parse options and arguments |
| 123 | +while [ -n "$1" ]; do |
| 124 | + case "$1" in |
| 125 | + '-h'|'--help'|'') |
| 126 | + Help |
| 127 | + exit 0 |
| 128 | + ;; |
| 129 | + '-V'|'--version') |
| 130 | + printf '%s v%s\n' "$SCRIPT_NAME" "$VERSION" |
| 131 | + exit 0 |
| 132 | + ;; |
| 133 | + '-I'|'--range'|'-E'|'--exclude-range') |
| 134 | + [ -n "$2" ] && [ -n "$3" ] || Fatal "'$1' requires two arguments." |
| 135 | + IsInt "$2" && IsInt "$3" || Fatal "Both '$2' and '$3' must be integers." |
| 136 | + [ $2 -le $3 ] || Fatal "'$2' must be <= '$3'." |
| 137 | + |
| 138 | + if [ "$1" = '--range' ] || [ "$1" = '-I' ] ; then # include range |
| 139 | + INCLUDE_RANGE="${INCLUDE_RANGE:+${INCLUDE_RANGE}${NEWLINE}}${2}-${3}" |
| 140 | + else # exclude range |
| 141 | + EXCLUDE_RANGE="${EXCLUDE_RANGE:+${EXCLUDE_RANGE}${NEWLINE}}${2}-${3}" |
| 142 | + fi |
| 143 | + shift 2 |
| 144 | + ;; |
| 145 | + '-i'|'--include'|[!-]*) # specific IDs to include |
| 146 | + while IsInt "$1"; do |
| 147 | + INCLUDE="${INCLUDE:+${INCLUDE}${NEWLINE}}$1" |
| 148 | + shift 1 |
| 149 | + done |
| 150 | + ;; |
| 151 | + '-e'|'--exclude') # specific IDs to exclude |
| 152 | + [ -n "$2" ] || Fatal "'$1' requires an argument." |
| 153 | + while IsInt "$2"; do |
| 154 | + EXCLUDE="${EXCLUDE:+${EXCLUDE}${NEWLINE}}$2" |
| 155 | + shift 1 |
| 156 | + done |
| 157 | + ;; |
| 158 | + '-f'|'--file') # file to parse |
| 159 | + [ -n "$2" ] || Fatal "'$1' requires an argument." |
| 160 | + FILE_TO_PARSE=$2 && shift 1 |
| 161 | + |
| 162 | + # shdwfilter needs both passwd and shadow files |
| 163 | + if [ -z "${SCRIPT_NAME##shdwfilter*}" ]; then |
| 164 | + [ -n "$2" ] || Fatal "'$1' requires both passwd and shadow files." |
| 165 | + SHADOW_TO_PARSE=$2 && shift 1 |
| 166 | + fi |
| 167 | + ;; |
| 168 | + -*) |
| 169 | + Fatal "'$1' is not a valid '$SCRIPT_NAME' option.";; |
| 170 | + esac |
| 171 | + [ -n "$1" ] && shift 1 # only shift if there's anything left to shift |
| 172 | +done |
| 173 | + |
| 174 | +# |
| 175 | +# populate *_CONTENTS, either from piped input or file(s) |
| 176 | +# |
| 177 | +# TODO: if -f was set, no piped input should be parsed |
| 178 | +if [ -p /dev/stdin ]; then # piped input |
| 179 | + [ -z "${SCRIPT_NAME##shdwfilter*}" ] && Fatal "'shdwfilter' cannot handle piped input." |
| 180 | + while read P_LINE; do |
| 181 | + FILE_CONTENTS="${FILE_CONTENTS:+${FILE_CONTENTS}${NEWLINE}}${P_LINE}" |
| 182 | + done |
| 183 | +else # read in the appropriate file(s) |
| 184 | + [ -r "$FILE_TO_PARSE" ] || Fatal "'$FILE_TO_PARSE' is not readable!" |
| 185 | + FILE_CONTENTS=`cat "$FILE_TO_PARSE"` || Fatal "Failed to 'cat' file: '$FILE_TO_PARSE'" |
| 186 | + |
| 187 | + if [ -z "${SCRIPT_NAME##shdwfilter*}" ]; then |
| 188 | + [ -r "$SHADOW_TO_PARSE" ] || Fatal "'$SHADOW_TO_PARSE' is not readable!" |
| 189 | + SHADOW_CONTENTS=`cat "$SHADOW_TO_PARSE"` || Fatal "Failed to 'cat' shadow file: '$SHADOW_TO_PARSE'" |
| 190 | + fi |
| 191 | +fi |
| 192 | + |
| 193 | +# |
| 194 | +# loop over and parse *_CONTENTS; |
| 195 | +# print desirable IDs |
| 196 | +# |
| 197 | +for F_LINE in $FILE_CONTENTS; do |
| 198 | + # get the third item (UID/GID) in the line |
| 199 | + first_three=${F_LINE#*:*:} |
| 200 | + the_id=${first_three%%:*} |
| 201 | + |
| 202 | + IsInt "$the_id" || Fatal "'$the_id' is not a valid id number." |
| 203 | + DesiredID "$the_id" || continue |
| 204 | + |
| 205 | + if [ -z "${SCRIPT_NAME##shdwfilter*}" ]; then |
| 206 | + user_name=${F_LINE%%:*} # first item (user name) in the passwd line |
| 207 | + for S_LINE in $SHADOW_CONTENTS; do |
| 208 | + [ "$user_name" = ${S_LINE%%:*} ] && printf '%s\n' "$S_LINE" && break |
| 209 | + # believe it or not, the above is actually ~1.4x faster than: |
| 210 | + # [ -z "${S_LINE##${user_name}:*}" ] |
| 211 | + done |
| 212 | + # TODO: If no match, error? |
| 213 | + else # password- or group-filter |
| 214 | + printf '%s\n' "$F_LINE" |
| 215 | + fi |
| 216 | +done |
| 217 | + |
| 218 | +# restore original IFS |
| 219 | +IFS=$ORIG_IFS |
0 commit comments