#!/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,(> "$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}"