-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathgroup-matrix
executable file
·243 lines (195 loc) · 8.62 KB
/
group-matrix
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
#!/bin/bash
#####################################################################################
#
# The MIT License (MIT)
#
# Copyright (c) 2015-2017 Jules Kerssemakers / Deutsches Krebsforschungszentrum Heidelberg (DKFZ)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
# OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
#####################################################################################
# This script shows a membership matrix of all users in the queried groups
# check invocation
if [ $# -lt 1 ]; then
echo "Not enough arguments! (expected one or more AD-groups)"
# usage string: indented with tab instead of ' ' to deal with HEREDOC indenting rules
cat <<-EOS
USAGE: $0 AD_GROUP [AD_GROUP...]
Prints a matrix showing which members are in which group, and if those accounts are expired or not.
The script outputs pretty-printed tables if the output is the terminal
If the output goes to a file or pipe, a properly-quoted CSV format is emitted instead
Example terminal output:
# ./group-matrix group1 group2 group3
USERS FULL_NAME STATUS group1 group2 group3
user1 Randomuser, Sam active Y
user2 Randomuser, Jay EXPIRED Y Y Y
user3 Randomuser, Beau active Y
user4 Randomuser, Alex active Y Y
Example redirected output:
# ./group-matrix group1 group2 group3 | cat
USERS,"FULL_NAME",STATUS,group1,group2,group3
user1,"Randomuser, Sam",active,,Y,
user2,"Randomuser, Jay",EXPIRED,Y,Y,Y
user3,"Randomuser, Beau",active,Y,,
user4,"Randomuser, Alex",active,Y,Y,
Please note that input groups are sorted and de-duplicated, so output column order
will not necessarily match the order specified on the command line.
EOS
exit 1
fi
# Load the secrets such as password and hostname from somewhere not-in-git
source "${XDG_CONFIG_HOME:-"$HOME"/.config}/ldap-summary.conf"
###################################################################################
# Function definitions
###
# unique-ifies the bash array of its input (`uniq` built-in only does lines of files)
# Thank you StackOverflow for the deduplication logic:
# https://stackoverflow.com/questions/13648410/how-can-i-get-unique-values-from-an-array-in-bash
# specifically this answer: https://stackoverflow.com/a/13648438/2964696
function unique() {
echo "$@" | tr ' ' '\n' | sort -u | tr '\n' ' '
}
###
# prints the usernames in a group to stdout
ldapmembers() {
GRP=$1;
ldapsearch -u -LLL \
-h "$JK_LDAPHOST" -w "$JK_LDAPPASSWORD" \
-D "$JK_BINDDN" -b "$JK_SEARCHBASE" \
-x \
cn="$GRP" member \
| sed -r -e 's/\r?\n //g' -e 's/^member: CN=(\w+?),.+$/MEMBER:\1/' \
| grep '^MEMBER:' \
| sed 's/^MEMBER://' \
| sort
}
###
# given a group-name, generates the (re-used) tmp-file path
# for the file containing that group's members
membertmp() {
echo "$TMPDIR/members-$1.txt"
}
###################################################################################
# We will generate lots of temp-files. keep them all in one place for easy cleanup later
TMPDIR=$(mktemp -d --suffix="-group-matrix-tmp")
# column-separator in tmp-files
SEPARATOR='#'
###################################################################################
# Which groups to process?
# First: sort and deduplicate the groups. This is necessary, because processing
# a group multiple times leads to re-use of the groups' old tempfile, which resets
# the progress to a previous state, thus losing results.
GRPS=($( unique "$@") )
###################################################################################
# obtain the users in each individual group
# intermediate tracking: all users we've seen
# needed for consistent sort-ordering later on
declare -a ALL_USERS
for GRP in ${GRPS[*]}; do
GRP_MEMBERS=$(membertmp $GRP)
# Header
echo "USERS${SEPARATOR}${GRP}" > "$GRP_MEMBERS"
members=( $(ldapmembers $GRP | sort ) )
ALL_USERS=( "${ALL_USERS[*]}" "${members[*]}" ) # creates a lot of redundancy, do a batch `unique` below
# create a group-specific file: "username<SEPARATOR>Y"
# This will make for a more compact table after joining.
for U in ${members[*]}; do
echo "${U}${SEPARATOR}Y" >> "$GRP_MEMBERS"
done
done;
# batch `unique`: _distinct_ usernames only
ALL_USERS=( $(unique "${ALL_USERS[*]}") )
###################################################################################
# see which users are expired
# current time, in LDAP timestamp format.
# magic numbers:
# 11644473600 = 1.1.1600 (windows epoch) -> 1.1.1970 (linux epoch) difference in seconds
# 10 000 000 = factor for (windows) 100-nanosecond resolution from (linux) seconds
AD_NOW=$(( ($(date '+%s') + 11644473600 ) * 10000000 ))
ALL_USERS_UNIQ_WITH_STATUS="$TMPDIR/user-status.txt"
echo "USER${SEPARATOR}FULL_NAME${SEPARATOR}STATUS" > "$ALL_USERS_UNIQ_WITH_STATUS" # header
for U in ${ALL_USERS[*]}; do
# single round-trip to ldap-server for all user-related info
ldap_answer=$(ldapsearch -u -LLL \
-h "$JK_LDAPHOST" -w "$JK_LDAPPASSWORD" \
-D "$JK_BINDDN" -b "$JK_SEARCHBASE" \
-x \
cn="$U" accountExpires displayName \
| sed -r -e 's/\r?\n //g' \
| grep -e '^accountExpires:' -e '^displayName:'
)
# extract and process expiry timestamp to determine account status
expire_time=$( grep '^accountExpires' <<< "$ldap_answer" | sed 's/accountExpires: //' )
status='active'
if [ "$expire_time" -lt "$AD_NOW" ]; then
status='EXPIRED'
fi
# extract and process displayName
displayName=$( grep '^displayName' <<< "$ldap_answer" )
# displayName is base64 encoded when it contains non-ASCII
if [[ $displayName =~ :: ]]; then # '::' signals base64-encoded content
displayName=$( sed 's/displayName:: //' <<< "$displayName" | base64 -d )
else
displayName=$( sed 's/displayName: //' <<< "$displayName" )
fi
echo "${U}${SEPARATOR}${displayName}${SEPARATOR}${status}" >> "$ALL_USERS_UNIQ_WITH_STATUS"
done
###################################################################################
# "recursively" join all member-files to the user index column
PREV="$ALL_USERS_UNIQ_WITH_STATUS" # seed with our username/expired-status table
for GRP in ${GRPS[*]}; do
# input and output for this iteration
CURRENT=$(membertmp $GRP)
INTERMEDIATE="$TMPDIR/jointmp-$GRP.txt"
# actual join
# We will join the different groups on their respective columns 1 (usernames), but emit only column 2 ('Y').
# This 'clever' join-ing leaves only a "Y(es)" in each group-column if the user is a member
# NB: `-o auto -e' '` is REQUIRED to make the alignment work
# otherwise, empty columns will be overlooked by the pretty-printing later
# leading to incorrect results (displaying incorrect group memberships)
join \
--header \
-t"${SEPARATOR}" \
-j1 \
-a1 -a2 \
-o auto -e ' ' \
"$PREV" "$CURRENT" > "$INTERMEDIATE"
# advance iteration
PREV="$INTERMEDIATE"
done
###################################################################################
# print final output, dependant on terminal or pipe/file
if [ -t 1 ]; then
# we're outputting to a terminal, pretty-print
column --separator "${SEPARATOR}" --table "$PREV"
else
# we're outputting to a file or pipe, create a (properly quoted) CSV
awk --posix --field-separator "${SEPARATOR}" --source='
BEGIN { OFS=","; } # output separator = CSV-comma
{
# apply CSV quoting according to W3C spec/RFC4180:
# GOTCHA: assigning to $2 recomputes $0, sneakily triggering the separator conversion
gsub("\"", "\"\"", $2) # double-quotes inside field are escaped by doubling them
$2 = "\"" $2 "\""; # entire field surrounded by double-quotes
print $0;
}' \
"$PREV"
# sed -E -e "s/${SEPARATOR}/,/g" "$PREV"
fi
# clean-up our tmp-files
rm -rf "$TMPDIR"