summaryrefslogblamecommitdiffstats
path: root/core/modules/pam-bwidm/data/opt/openslx/scripts/pam_bwidm
blob: 63787309d0ac77ab986e57264c1f91cad6421f7b (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13












                                                                                                             
                                 




                                                                      
                                         
                                                                   




                                                       




                                                                        





                                                                                 
                                             
                                         

                                         


                                                             










                                                                                   


                                                                     
   
                
   
                                       
                         


                                
                                        
                            
                                                 








                                                                         






                                                                                                                     
                                              

                                        
                                                          









                                                                                    



                                                                                             
 






                                                                                                                                                                                              
                                                                            



                                                                                                                 
          








                                                                                                                
          
            



                               
                                                                                    




















                                                                                                                                   
                                                                        






                                                       





                                             
                                                                     


                                
                                            

                                                                           



















                                                                                           
                                                                   











































                                                                                                                  

                                                                        
                                



                                                                                      

                                            
                                                                  
                                                                           
                                                          
                                   
                    
                                           



                                                                                                                     
 
                                                                         


                                                                                                                  
          

              
 
                                       
                                 














                                                                                             
                          



























                                                                                                
 


                                                                                           
                  



              
                                    









                                                                                     
   
                    
   

              
                             







                                                                                             
#!/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 [ "$PAM_TYPE" = "auth" ]; 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" ] || [ "${PAM_USER}" = "${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 [ "${SLX_BWIDM_AUTH}" = "yes" ]; then
	: # Allow everything
elif [ "${SLX_BWIDM_AUTH}" = "selective"  ]; 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 [ "${SLX_BWIDM_AUTH}" = "selective" ]; then
	FOUND=
	for org in ${SLX_BWIDM_ORGS}; do
		if [ "$org" = "$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 ] || [ "${idpret:0:1}" != "2" ]; 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 [ "$PAM_TYPE" = "auth" ]; 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 [ "$PAM_TYPE" = "account" ]; 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 [ "$mainret" = "7" ]; 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}"