summaryrefslogtreecommitdiffstats
path: root/core/modules/pam-bwidm/data/opt/openslx/scripts/pam_bwidm
blob: 0d3ccc1e9b0c8c9d8817bec4f8ef2eaa096987ce (plain) (blame)
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
#!/bin/ash
#
# This script is to be called by PAM (specifically pam_exec).
# We expect the username in the form: username@organisation
# If it is in that form, we will query the masterserver for the list
# of supported IdPs and if one matches the user's organisation
# we will try to authenticate against it.

# fix PATH as PAM clears it
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/openslx/sbin:/opt/openslx/bin"

# grab the password from stdin asap, since there is no guarantee some tool just reads it
unset USER_PASSWORD
if [ "x$PAM_TYPE" = "xauth" ]; then
	read -r USER_PASSWORD > /dev/null 2>&1
	readonly USER_PASSWORD
	[ -z "$USER_PASSWORD" ] && echo "No password given." && exit 1
fi

# sanity check on PAM_USER: contains '@'?
if [ -z "$PAM_USER" ] || [ "x${PAM_USER}" = "x${PAM_USER%@*}" ]; then
	# no @ contained, invalid username, abort
	#echo "Invalid username '$PAM_USER'. Aborting."
	exit 1
fi

if ! busybox which curl || ! busybox which mktemp; then
	echo "'curl/mktemp' missing. This script won't work without it."
	exit 1
fi

# determine proper tmp dir, prefer one in RAM
for TMPDIR in "/run" "/run/user/$(id -u)" "/dev/shm" "/home/$(whoami)" "/tmp"; do
	[ -d "$TMPDIR" ] && [ -w "$TMPDIR" ] && [ -r "$TMPDIR" ] && break
done
readonly TMPDIR

# redirect stdout/stderr to temporary logfile
readonly LOGFILE="$(mktemp -p "$TMPDIR")"

# Determine mode: ECP or Embedded Browser
IDP_QUERY_URL="$( awk -F= '$1 ~ /^shib-url\s*$/ {print $2}' \
	/etc/lightdm/qt-lightdm-greeter.conf \
	/etc/lightdm/qt-lightdm-greeter.conf.d/*.conf \
	| tail -n 1 | xargs )" # TRIM

if [ -n "$IDP_QUERY_URL" ]; then
	mode=browser
else
	# URL to query masterserver for IDPs
	IDP_QUERY_URL="https://bwlp-masterserver.ruf.uni-freiburg.de/webif/pam.php"
	mode=ecp
	readonly IDP_QUERY_CACHE="/run/openslx/bwlp-idp"
fi
readonly IDP_QUERY_URL mode

# everything in a subshell in an effort to hide sensitive information
# from this script's environment
###
( # <-- subshell
###
# redirect stdout and stderr to logfile
#exec > "${LOGFILE}" 2>&1

# check if we are allowed to run
. /opt/openslx/config
if [ "x${SLX_BWIDM_AUTH}" = "xyes" ]; then
	: # Allow everything
elif [ "x${SLX_BWIDM_AUTH}" = "xselective"  ]; then
	if [ -z "${SLX_BWIDM_ORGS}" ]; then
		echo "bwIDM selective mode with empty org list - exiting"
		exit 1
	fi
else
	echo "bwIDM login disabled in openslx-config."
	exit 1
fi

# valid username, we can already split it here
readonly USER_USERNAME="${PAM_USER%@*}"
readonly USER_ORGANISATION="${PAM_USER#*@}"
[ -z "$USER_ORGANISATION" ] && echo "Could not parse organisation from given login: ${PAM_USER}. Aborting." && exit 1
[ -z "$USER_USERNAME" ] && echo "Could not parse user from given login: ${PAM_USER}. Aborting." && exit 1

# Check if we're in selective mode and if so, whether the user's organization is whitelisted
if [ "x${SLX_BWIDM_AUTH}" = "xselective" ]; then
	FOUND=
	for org in ${SLX_BWIDM_ORGS}; do
		if [ "x$org" = "x$USER_ORGANISATION" ]; then
			FOUND=ya
			break
		fi
	done
	if [ -z "$FOUND" ]; then
		echo "bwIDM organization $USER_ORGANISATION not in whitelist, abort"
		exit 1
	fi
fi

if [ "$mode" = "ecp" ]; then
	# The given username is valid. Now we get the list of IdPs from the bwlp masterserver
	# and try to find the user's organisation
	mkdir -p /run/openslx

	# check if we have a (non-zero bytes) cached copy of the list
	if ! [ -s "${IDP_QUERY_CACHE}" ]; then
		if ! [ -w "/run/openslx" ]; then
			echo "No IDP info cached, cache path not writable for current user."
			exit 7
		fi
		idpret="$(curl --retry 3 --retry-connrefused --retry-delay 1 --retry-max-time 15 -w "%{http_code}" -o "${IDP_QUERY_CACHE}" --connect-timeout 2 --max-time 6 "$IDP_QUERY_URL")"
		if [ "${#idpret}" != 3 ] || [ "x${idpret:0:1}" != "x2" ]; then
			echo "Could not download the list of identity providers from '$IDP_QUERY_URL'. Aborting."
			rm -f -- "$IDP_QUERY_CACHE"
			exit 7
		fi
	fi
	# here we have the cache for sure, search for the given organisation's ECP URL
	USER_ECP_URL="$(awk -v idp="${USER_ORGANISATION}" -F '=' '{if($1==idp) print $2}' < "$IDP_QUERY_CACHE")"
	[ -z "$USER_ECP_URL" ] && echo "Could not determine ECP URL for '${USER_ORGANISATION}'" && exit 1

	# path to the SOAP envelope we are gonna need soon
	readonly SOAP_ENVELOPE="/opt/openslx/bwidm_soap.xml"
	if ! [ -f "${SOAP_ENVELOPE}" ]; then
		echo "Failed to find the SOAP envelope at '${SOAP_ENVELOPE}'. Aborting."
		exit 1
	fi
fi # ECP end

# recap: here we have validated
#	- username
#	- organisation
#	- ECP URL for that organisation, if using ECP mode, otherwise this is a NOOP

# now create the bwidm group: find the first free GID from 1000 "downwards" to 100
BWIDM_GROUP="$(getent group bwidm)"
if [ -z "$BWIDM_GROUP" ]; then
	BWIDM_GID=999
	while [ "$BWIDM_GID" -gt 100 ]; do
		getent group "$BWIDM_GID" || break
		let BWIDM_GID--
	done
	if [ "$BWIDM_GID" -eq 100 ]; then
		# use demo's gid as a fallback
		readonly BWIDM_GID="$(id -g "demo")"
		[ -z "$BWIDM_GID" ] && echo "Could not determine the GID of 'demo'. Cannot use it as fallback. Aborting." && exit 1
	fi

	# now create the group
	if ! echo "bwidm:x:$BWIDM_GID:" >> /etc/group; then
		echo "Could not create 'bwidm' group with gid '$BWIDM_GID'. Aborting."
		exit 1
	fi
else
	readonly BWIDM_GID="$(printf "%s" "$BWIDM_GROUP" | cut -d: -f3)"
fi
if [ -z "$BWIDM_GID" ]; then
	echo "Could not determine BWIDM-GID. Aborting."
	exit 1
fi
readonly USER_GID="$BWIDM_GID"

auth_user() {
	if [ "$1" = "--ignore-errors" ]; then
		ignore_errors=1
		shift
	fi
	if [ "$#" -ne 2 ]; then
		echo "auth_user() requires 2 arguments, $# given: $*"
		exit 7
	fi
	# generate soap envelope
	NOW=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
	RID="_c${RANDOM}a${RANDOM}f${RANDOM}f${RANDOM}e${RANDOM}e${RANDOM}"
	RID="${RID:0:32}"
	REQUEST="$(sed "s/%TIMESTAMP%/${NOW}/g;s/%REQUESTID%/${RID}/g" "${SOAP_ENVELOPE}")"
	# set credentials in netrc file
	echo "machine ${HOST} login $1 password $2" > "${NETRC}"
	local ret="$(mktemp)"
	# do auth
	local cret="$(curl \
		--silent \
		--connect-timeout 5 \
		--max-time 15 \
		--output "$ret" \
		--data "$REQUEST" \
		--header "$CT" \
		--netrc \
		--netrc-file "$NETRC" \
		--write-out "%{http_code}" \
		"$USER_ECP_URL"
	)"
	echo "machine $HOST login $1 password *******************************" > "$NETRC"

	# check for valid http return code
	if ! [ "${#cret}" -ne 3 ] || [ "${cret:0:1}" != "2" ]; then
		# auth ok?
		local saml_ns_prefix="urn:oasis:names:tc:SAML:2.0:status"
		sed -ri 's,(</?)\w+:,\1,g' "$ret"
		# see http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
		local auth_status_xpath="/Envelope/Body/Response/Status/StatusCode"
		local auth_status_code="$(xmlstarlet sel -t -v "${auth_status_xpath}/@Value" "$ret")"
		if [ "$auth_status_code" = "${saml_ns_prefix}:Success" ]; then
			rm -f -- "$ret"
			return 0
		fi
		if [ "$auth_status_code" = "${saml_ns_prefix}:Requester" ]; then
			# error on our side, possibly wrong password
			local requester_status_code="$( \
				xmlstarlet sel -t -v \
				"${auth_status_xpath}[@Value=\"${saml_ns_prefix}:Requester\"]/StatusCode/@Value" \
				"$ret"
			)"
			if [ "$requester_status_code" = "${saml_ns_prefix}:AuthnFailed" ]; then
				# just wrong password, make PAM fail
				rm -f -- "$ret"
				return 1
			fi
		fi
	fi
	if [ -n "$ignore_errors" ]; then
		echo "Ignoring..."
		return 1
	fi
	# otherwise some internal error occured, append it to logfile
	(
	echo '########## IdP Response ##########'
	if xmlstarlet -q validate "$ret"; then
		# pretty print xml docs
		xmlstarlet format "$ret"
	else
		# dump as is
		cat "$ret"
	fi
	echo '########## END Response ##########'
	) >> "$LOGFILE"
	rm -f -- "$ret"
	exit 7
}

write_user_data() {
	# auth succeeded, create and map a local user to this bwIDM user
	local gexp LOOPS usrname
	echo "Login for '$USER_USERNAME' on '$USER_ORGANISATION' succeeded."
	gexp="$( printf "%s" "${PAM_USER}" | sed 's/[][$^\.*]/\\&/g' )" # Basic regexp
	if ! grep -q "^${gexp}:" /etc/passwd; then
		# create a random 6digit UID
		LOOPS=10
		while [ "$LOOPS" -gt 0 ]; do
			USER_UID="$(( 100000 + RANDOM % 100000 ))"
			# check existence of this UID, if it's free, use it
			getent passwd "$USER_UID" || break
			let LOOPS--
		done
		if [ "$LOOPS" -eq 0 ]; then
			# could not find an empty random 6-digit UID, so we will use demo's UID...
			USER_UID="$(id -u demo)"
			[ -z "$USER_UID" ] && echo "Could not use UID of 'demo' as a fallback, aborting..." && exit 1
		fi

		# we have a uid, gid, lets just create the local user now
		# mark with @ecp or @browser
		usrname="${PAM_USER}@${mode}"
		echo "${PAM_USER}:x:${USER_UID}:${USER_GID}:${usrname}:/home/${PAM_USER}:/bin/bash" >> /etc/passwd
	fi
	exit 0
}

# now the pam-type specific part starts
if [ "x$PAM_TYPE" = "xauth" ]; then
	##### Browser
	if [ "$mode" = "browser" ]; then
		token="${USER_PASSWORD#shib=}"
		if [ "${#USER_PASSWORD}" -gt 18 ] && [ "${USER_PASSWORD}" != "$token" ]; then
			nc="$(curl \
				--silent \
				--connect-timeout 5 \
				--max-time 15 \
				--data-urlencode "token=${token}" \
				"${IDP_QUERY_URL}?action=verify"
			)"
			err="${nc#ERROR=}"
			if [ "$err" != "$nc" ]; then
				echo "Shibboleth auth error: $err"
				exit 1
			fi
			user="${nc#USER=}"
			if [ "$user" = "$nc" ]; then
				echo "Invalid reply from SP"
				exit 1
			fi
			if [ "$user" != "$PAM_USER" ]; then
				echo "Shibboleth user mismatch: '$PAM_USER' != '$user'"
				exit 1
			fi
			write_user_data
		fi
	else
		##### ECP
		# set invariant parts of the requests
		readonly HOST=$(echo "${USER_ECP_URL}" | awk -F '/' '{print $3}')
		readonly CT='Content-Type: text/xml; charset=utf-8'
		NETRC=$(mktemp -p "$TMPDIR")
		[ -z "$NETRC" ] && NETRC="$TMPDIR/netrc_$$_${USER}_${RANDOM}.tmp"
		touch "$NETRC"
		chmod 0600 "$NETRC"

		# Now we are ready to actually send the credentials to the IdP.
		# To be sure that everything is working as expected, we will first auth
		# with a wrong password and expect a failure. Note that we don't car
		if auth_user --ignore-errors "$USER_USERNAME" "___invalid-INVALID++~"; then
			echo "Purposely wrong authentication succeeded, that should not happen."
			exit 7
		fi

		# auth failed as expected, proceed to auth user with the proper credentials
		if auth_user "$USER_USERNAME" "$USER_PASSWORD"; then
			write_user_data
		fi
	fi
	exit 1
fi

if [ "x$PAM_TYPE" = "xaccount" ]; then
	# the sanity checks we did before reacting to PAM_TYPE is enough to validate
	# the given username as a valid bwIDM username
	# ('@' contained and IdP found in the idp list fetched from the masterserver)
	# so just "accept"
	exit 0
fi

# script should never get to the following line
echo "$0 called for unsupported PAM_TYPE '$PAM_TYPE'. Aborting."
exit 1
###
) # <-- subshell end
# #
## main script
mainret=$?
if [ "x$mainret" = "x7" ]; then
	# exit code 7 is our marker to push the logfile to the sat
	slxlog --delete "pam-bwidm" "Internal error during bwIDM authentication" "${LOGFILE}"
	exit 1
else
	rm -- "${LOGFILE}"
fi
exit "${mainret}"