#!/bin/ksh -p
#
# ident "@(#)utpamcfg.sh	1.9 06/03/09 SMI"
#
# Copyright 2003,2005-2006 Sun Microsystems, Inc.  All rights reserved.
#
#
# utpamcfg - This script [un]configures the PAM configuration
#		file (normally /etc/pam.conf).
#
#    usage:
#
#	utpamcfg -c client[:module-type] [-o] [-m module [-M modargs]] [-t template] [-T tag] [-n true|false]
#
#		-c client[:module-type]
#		    Configure PAM stack for specified PAM client.  If
#		    module type is specified, only the stack for the
#		    specified module will be created.  Otherewise, it
#		    will create all the modules that are defined for
#		    the template.
#		-m module
#		    Add optional PAM module specified to the top
#		    of the PAM auth stack for this client's entry.
#		-M modargs
#		    Arguments to provide to PAM module specified by
#		    the -m switch.
#		-t template[:module-type]
#		    Use the PAM client specified as the template
#		    client PAM stack (default is to use "other").
#		    If a module type is specified, only entries for
#		    the specified module will be duplicated.
#		-T tag
#		    Use comment tag specified (default is to use
#		    "SunRay Server Software" (note no spacing
#		    between "Sun" and "Ray").
#		-n true|false
#		    true: use new pam_unix stack (S9 and later)
#		    false: use S8 pam_unix stack
#		-o
#		    If specified with the -t option, it will not use the module
#		    entries from the "other" service if the requested module
#		    is not defined for the service specified.  If this option
#		    is not specified, it will use the entries from the "other service.
#
#	utpamcfg -a client[:module-type] [-m module [-M modargs]] [-T tag] [-n true|false] [-f flag]
#
#               -a client[:module-type]
#                   Add module for client to the top of the stack.  The
#		    module type specifies the type of module is being added.
#		    For example, "account".  The default value is "auth".
#               -f flag
#                   The flag to be used for the pam line, eg: requisite, 
#                   sufficient etc
#		-c client
#		    Configure PAM stack for specified PAM client.
#		-m module
#		    Add optional PAM module specified to the top
#		    of the PAM auth stack for this client's entry.
#		-M modargs
#		    Arguments to provide to PAM module specified by
#		    the -m switch.
#		    between "Sun" and "Ray").
#		-n true|false
#		     true: use new pam_unix stack (S9 and later)
#		    false: use S8 pam_unix stack
#
#	utpamcfg -u client[:module-type] [-T tag]
#
#               -u client[:module-type]
#		    Unconfigure PAM stack for specified PAM client. The
#		    module type specifies the type of module is being unconfigured.
#		    For example, "account".  The default value is "all" type.
#		-T tag
#		    Remove comment tag specified (default same
#		    as above).
#
#	utpamcfg -l client [-T tag]
#
#		-l client
#		    List PAM stack for specified PAM client.
#		-T tag
#		    Display comment tag specified (default same
#		    as above).
#
#    undocumented switches:
#
#	utpamcfg [-d] [-P conffile]
#
#		-d
#		    Enables debugging messages.
#		-P conffile
#		    Specifies an alternative PAM configuration file.
#

# DESIGN NOTE: This script now handles /etc/pam.conf format
# configurations as well as /etc/pam.d/ directory-based
# configurations.  The format of the files in an /etc/pam.d/
# configuration is idential to the /etc/pam.conf format, except that
# the first field on each line in /etc/pam.conf is the "service name"
# (referred to in this script generally as the "client"), whereas in
# /etc/pam.d the service name is the name of the file itself, and that
# field is not included in the contents.  Systems (like Linux) that
# allow /etc/pam.d/ configurations also allow /etc/pam.conf
# configurations, and the rule is that if /etc/pam.d exists, then
# /etc/pam.conf is ignored.
#
# However, on Solaris /etc/pam.d handling is disabled, because some
# layered products create a spurious /etc/pam.d directory


    typeset -r _ALL_="all"
    mod="`basename $0`"
    pam_conf_sys="/etc/pam.conf"
    pam_dir="/etc/pam.d"
    if [ -d $pam_dir -a "`uname -s`" != SunOS ]; then
	isdir=true
    else
	isdir=false
    fi
    template_client=""
    default_template_client="other"
    SUNRAY_SOFT="SunRay Server Software"
    tag=""
    flag="sufficient"

    config="none"
    PAM_module=""
    PAM_module_args=""
    debugme=false;
    tmpfile="/tmp/$mod.$$"
    savefile="/tmp/$mod.save.$$"
    newpam=false
    
    # set umask so that our file creation is done with the correct perms
    umask 022

    # Setup trap handler to cleanup
    trap "cleanup; exit" 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 

    #
    # Display command line usage and optional descriptive text.
    #
    function usage {

	typeset msg="$1"

	echo "usage: $mod -c client[:type [-o]] [-m module [-M modargs] [-t template] [-T tag] [-n true|false]"
	echo "usage: $mod -u client[:type] [-T tag]"
	echo "usage: $mod -l client"
	echo "usage: $mod -a -c client[:type [-o]] [-m module [-M modargs] [-T tag] [-f flag]"


	if [ "$msg" != "" ] ; then
	    echo "usage: $mod $msg"
	fi

    }

    #
    # Clean up any leftovers in preperation to exit.
    #
    function cleanup {

	typeset ss="$status"

	debug "cleanup" "Swish swish swish. Wipe wipe wipe."

	rm -f $tmpfile
	rm -f $savefile

	debug "cleanup" "Garbage collected and expunged."

	status="$ss"
    }

    #
    # Display messages if debug is enabled
    #
    #	calling:
    #	    debug facility message
    #
    function debug {

	typeset fac="$1"
	typeset msg="$2"

	sep=": "

	if [ "$fac" == "" ] ; then
	    sep=""
	fi

	if [ $debugme == true ] ; then
	    echo "$fac$sep$msg"
	fi

    }

    #
    # Check if filename refers to a file and if it is writable
    # and readable.
    #
    #	calling:
    #	    checkfile filename mode
    #
    #	Notes: If mode == "-w" no write accessability check will
    #		be done on the specified filename.
    #
    function checkfile {

	typeset fn="$1"
	typeset mode="$2"

	typeset status=0

	debug "checkfile" "filename=[$fn] mode=[$mode]"

	if [ "$fn" == "" ] ; then
	    echo "$mod: no filename specified"
	    status=100
	    return
	fi

	if [ "$mode" = "-w" ] ; then
	    wrcheck=false
	    wrmsg=""
	else
	    wrcheck=true
	    wrmsg="writable|"
	fi

	if test ! -a "$fn" ; then
	    if $isdir ; then
	    	# in the pam.d case, it's possible that the conffile does
		# not exist.  This is for the case where we are creating a
		# new stack from scratch.
	    	debug "checkfile" "$fn is OK (no file in pam.d directory)"
	    else
	    	echo "$mod: $fn does not exist"
	    fi
	    status=101
	elif test ! -f "$fn" ; then
	    echo "$mod: $fn is not a regular file"
	    status=102
	elif test $wrcheck == true -a ! -w "$fn" ; then
	    echo "$mod: $fn is not writable"
	    status=103
	elif test ! -r "$fn" ; then
	    echo "$mod: $fn is not readable"
	    status=104
	else
	    debug "checkfile" "$fn is OK (exists|regular|${wrmsg}readable)"
	fi

	return
    }

    #
    # List PAM stack for client.
    #
    #	calling:
    #	    list config_file client tag
    #
    # Return non-zero status if stack does not exist
    #
    function list {

	typeset conffile="$1"
	typeset client="$2"
	typeset tag="$3"

	debug "list" "conffile=[$conffile] client=[$client] tag=[$tag]"

	checkfile "$conffile" "-w"
	if [ $status != 0 ] ; then
	    return 1
	fi

	if $isdir ; then
	    cat $conffile
	    status=$?
	else
	    egrep "$tag" $conffile | egrep "[ 	]*$client "

	    OUT=$(awk "
	        {
	    	if (\$1 == \"$client\")
		    printf( \$0 \"\n\" );
		}" $conffile 2>/dev/null
	    )
	    print "$OUT"
	    if [ -z "$OUT" ]; then
		status=1
	    fi
	fi
	return $status
    }

    #
    # Unconfig (remove) the client from the file.
    #
    #	calling:
    #	    unconfig config_file mod_list client client_type tag
    #
    function unconfig {
	typeset conffile="$1"
	typeset client="$2"
	typeset client_type="$3"
	typeset tag="$4"

	debug "unconfig" "conffile=[$conffile] client=[$client] tag=[$tag]"
	debug "unconfig" "client_type=[$client_type]"

	if $isdir && [ ! -f $conffile ] ; then
	    # Already unconfigured
	    return
	fi

	checkfile "$conffile"
	if [ $status != 0 ] ; then
	    return
	fi

	# unconfig the whole client stack
	if $isdir ; then
	    if [ "$client_type" == "$_ALL_" ]; then
	    	rm -f $pam_conf
	    else
		# remove only the module type specified
	    	awk "{
		    if ((\$1 != \"$client_type\"))
		    	printf( \$0 \"\n\" );
		    }" $pam_conf >$tmpfile 2>/dev/null
		cat $tmpfile > $pam_conf
	    fi
	    return
	else
	    awk "{
	            if ((\$1 != \"$client\") || \
			((\$2 != \"$client_type\") && (\"$client_type\" != \"all\"))) {
		        printf( \$0 \"\n\" );
		    }
	        }" $conffile >$tmpfile 2>/dev/null
	fi

	if [ "$?" = 0 ] ; then
	    if [ -n "$tag" ];then
		egrep -v "$tag" $tmpfile >$conffile
	    else
		cat $tmpfile >$conffile
	    fi
	    rm $tmpfile
	    debug "unconfig" "$conffile updated"
	else
	    rm $tmpfile
	    usage "Error updating $conffile"
	    status=5
	fi

	return
    }

    #
    # Config (add) the client to the file.
    #
    #	calling:
    #	    config config_file client client_type module modargs tag template prefix
    #
    function config {
	typeset conffile="$1"
	typeset client="$2"
	typeset client_type="$3"
	typeset module="$4"
	typeset modargs="$5"
	typeset tag="$6"
	typeset template="$7"
	typeset prefix="$8"

	# XXX Note that we must remove any existing "module" line if it
	# XXX is in the template stack so that we can replace it with
	# XXX the (possibily identical) module + modargs line specified.

	debug "config" "conffile=[$conffile] client=[$client]"
	debug "config" "module=[$module] modargs=[$modargs]"
	debug "config" "tag=[$tag] template=[$template] prefix=[$prefix]"
	debug "config" "client_type=[$client_type]"

	# first unconfig any old entries
	unconfig "$conffile" "$client" "$client_type" "$tag"
	if [ $status != 0 ] ; then
	    return
	fi

	if [ $isdir == false ]; then
	    # check if the file is still accessable just in case
	    # unconfig did something unsociable to it
	    checkfile "$conffile"
	    if [ $status != 0 ] ; then
		return
	    fi
	fi

	#
	# Create the PAM stack for this client.
	# create_stack returns the stack in $client_stack
	#
	client_stack=""
	create_stack "$conffile" "$prefix" "$template" "$client_type"
	if [ $status != 0 ] ; then
	    echo "$mod: unable to create stack for $client"
	    return
	fi

	# add in optional module
	if [ "$module" != "" ] ; then
	    add_module "$client" "$prefix" "$module" "$modargs" "$client_stack"
	fi

	if [ -n "$tag" ]; then
		add_tag "$client" "$tag" "$client_stack"
	fi

	debug
	debug "" "------------------------------"
	debug "config" "PAM stack for $client:"
	debug "" "$client_stack"
	debug "" "------------------------------"
	debug

	# actually update the PAM configuration file
	echo "$client_stack" >>$conffile

	return
    }

    #
    # Create a PAM stack for the client - base it on the template
    # PAM client if that was specified, otherwise just use the entries
    # for the "$default_template_client" client. If that doesn't exist,
    # return some reasonable default stack that should work.
    #
    # Notes: Depends on $newpam to be set to determine which type
    #	of default stack to return.
    #
    #	calling:
    #	    create_stack config_file prefix template_client client_type
    #
    #	returns:
    #	    client stack in $client_stack variable
    #
    function create_stack {
	typeset conffile="$1"
	typeset prefix="$2"
	typeset template="$3"
	typeset client_type="$4"

	typeset status=0
	typeset procfiles

	if $isdir ; then
	  procfiles="$conffile $pam_dir/$default_template_client"
	else
	  procfiles=$conffile
	fi

	for cfile in $procfiles
	  do
	  if $isdir ; then
	      debug "create_stack" "cfile = [$cfile]"
	      if [ -f $cfile ]; then
		  if [ "$client_type" != "$_ALL_" ]; then
		      awk "{
			if ((\$1 == \"$client_type\"))
		    	    printf( \$0 \"\n\" );
			}" $cfile >$tmpfile 2>/dev/null
		  else
		      cat $cfile > $tmpfile
		  fi
		  if [ -s "$tmpfile" ] ; then
		  	client_stack="`cat $tmpfile`"
		  	debug "create_stack" "found stack in [$cfile] for [$client]"
			rm -f $tmpfile
		  	return
		  fi
		  rm -f $tmpfile
	      fi
	  else
	      for cname in $template $default_template_client
		do
		debug "create_stack" "cname = [$cname] cfile = [$cfile]"

		awk "{
		    if ((\$1 == \"$cname\") && ((\"$client_type\" == \"$_ALL_\") || \
		       (\$2 == \"$client_type\"))) {
			    printf(\"${prefix}\");
			    for (i = 2; i <= NF; i++)
			    printf(\$i \" \");
			    printf(\"\n\");
			}
		}" $cfile >$tmpfile 2>/dev/null

		if [ -s "$tmpfile" ] ; then
		    client_stack="`cat $tmpfile`"
		    debug "create_stack" "found stack in [$cfile] for [$cname]"
		    rm -f $tmpfile
		    return
		fi
		rm -f $tmpfile
	      done
	    fi
	done

	# default template stack - couldn't find template or "other"
	# to copy.
	# XXX This pre-existing logic is pretty bad, actually - we
	# shouldn't be forcing any particular stack since the overall
	# stack pattern is not stable from Solaris release to release,
	# let alone Linux.  What's "new" for S9 will be old for S9+
	# ...  The good news is that for Linux this case never arises,
	# there always exists an "other" stack (probably true for
	# Solaris these days also).
	if [ $newpam == true ] ; then
	    client_stack="${prefix}auth requisite pam_authtok_get.so.1\n"
	    client_stack="${client_stack}${prefix}auth required  pam_dhkeys.so.1\n"
	    client_stack="${client_stack}${prefix}auth required  pam_unix_auth.so.1"
	else
	    client_stack="${prefix}auth required /usr/lib/security/\$ISA/pam_unix.so.1"
	fi

	return
    }

    #
    # Add in the optional module.
    #
    #	calling:
    #	    add_module client prefix module modargs client_stack
    #
    #	returns:
    #	    updated client stack in $client_stack variable
    #
    function add_module {

	typeset client="$1"
	typeset prefix="$2"
	typeset module="$3"
	typeset modargs="$4"
	typeset cs="$5"

	debug "add_module" "client=[$client] prefix=[$prefix] module=[$module] modargs=[$modargs]"

	client_stack="${prefix}auth sufficient $module $modargs\n$cs"

	return
    }

    #
    # Add in the optional tag as a comment.
    #
    #	calling:
    #	    add_tag client tag client_stack
    #
    #	returns:
    #	    updated client stack in $client_stack variable
    #
    function add_tag {

	typeset client="$1"
	typeset tag="$2"
	typeset cs="$3"

	debug "add_tag" "client=[$client] tag=[$tag]"

	client_stack="# added to $client by $tag\n$cs"

	return
    }

    #
    # Save the current configuration for the given client from
    # the configuration file
    #
    #	calling:
    #	    saveconfig configfile client client_type
    #
    #
    function saveconfig {
	typeset conffile="$1"
	typeset client="$2"
	typeset client_type="$3"

	debug "saveconfig" "conffile=[$conffile] client=[$client] client_type=[$client_type]"

	checkfile "$conffile"
	if [ $status != 0 ] ; then
	    return
	fi
	if $isdir ; then
	    if [ "$client_type" == "$_ALL_" ];then
	    	cp $conffile $savefile
	    else
		awk "{
		    if (\$1 == \"$client_type\") {
			printf (\$0 \"\n\" );
		    }
	        }" $conffile >$savefile 2>/dev/null
	    fi
	else
	    awk "{
		if ((\$1 == \"$client\") && ((\"$client_type\" == \"$_ALL_\") || \
		       (\$2 == \"$client_type\"))) {
		    printf( \$0 \"\n\" );
		}
	    }" $conffile >$savefile 2>/dev/null
	fi
	return
    }

    #
    # Add in a module to the top of the stack for the given 
    # client. Can be enhanced to say at what position, currently
    # always adds to the top of the stack
    #
    #  add_this_module conffile client client_type PAM_module PAM_module_args tag flag prefix
    #
    function add_this_module {
	typeset conffile="$1"
	typeset client="$2"
	typeset client_type="$3"
	typeset PAM_module="$4"
	typeset PAM_module_args="$5"
	typeset tag="$6"
	typeset flag="$7"
	typeset prefix="$8"

	debug "add_this_module" "call saveconfig"

	# save the current configuration for the client
	saveconfig "$conffile" "$client" "$client_type"
	# unconfigure the client from the conf file
	unconfig "$conffile" "$client" "$client_type" "$tag"

	if [ -n "$tag" ]; then
	    debug "add_this_module" "sending: $tag"
	    add_tag "$client" "$tag" ""
	fi

	debug "add_this_module" "sending: $client $client_type $flag $PAM_module $PAM_module_args"
	client_stack="${client_stack}${prefix}$client_type $flag $PAM_module $PAM_module_args"
	# echo the new module to the conffile
	if $isdir ; then
	    if [ -f $savefile ]; then
	    	# keep the pam version at the top of the file
	    	pam_ver="$(grep '^#%PAM' $savefile)"
		if [ -n "$pam_ver" ]; then
	    	    echo "$pam_ver" >> $conffile
		else
		    # version not specified, set it to random filter
		    pam_ver="HighlyUnlikelyString!%!"
		fi
	    fi
	else
	    # set a version that won't match the filter below
	    pam_ver="HighlyUnlikelyString!%!"
	fi
	echo $client_stack >> $conffile
	
	# append old configurations to the file
	# strip out any old tags and the pam version (emitted at top for pam.d/*)
	if [ -f $savefile ]; then
	    if [ -n "$tag" ]; then
	    	egrep -v "$tag|$pam_ver" $savefile >> $conffile
	    else
	    	egrep -v "$pam_ver" $savefile >> $conffile
	    fi
	fi

	rm -f $savefile

	return
    }

    #
    # Main code starts here
    #
    while getopts c:m:M:u:t:T:dP:n:l:a:f:o arg 2>/dev/null
      do

	case $arg in
	    a) PAM_opt="$OPTARG" ; config=add ;;
	    c) PAM_opt="$OPTARG" ; config=true ;;
	    m) PAM_module="$OPTARG" ;;
	    M) PAM_module_args="$OPTARG" ;;
	    u) PAM_opt="$OPTARG" ; config=false ;;
	    t) template_client="$OPTARG" ;;
	    o) default_template_client="";;
	    T) tag="$OPTARG" ;;
	    d) debugme=true ;;
	    P) pam_conf_new="$OPTARG" ;;
	    n) newpam="$OPTARG" ;;
	    l) PAM_client="$OPTARG" ; config=list ;;
	    f) flag="$OPTARG" ;;
	   \?) usage ; exit 1 ;;

	esac

      done

    case $newpam in
	t*|T*) newpam=true ;;
	f*|F*) newpam=false ;;
	    *) usage "Invalid option to -n" ; exit 2 ;;
    esac

    PAM_client=`print $PAM_opt | sed -n -e 's/\([^:]*\).*$/\1/p'`
    PAM_type=`print $PAM_opt | sed -n -e 's/^.*:\(.[.]*\)/\1/p'`
    if [ -z "$PAM_type" ]; then
	# module type not specified.  Default to "auth".
	if [ "$config" == "add" ]; then
	    # for backward compatibility, default to "auth" when adding
	    PAM_type="auth"
	else
	    PAM_type="$_ALL_"
	fi
    fi
    if [ -n "$pam_conf_new" ]; then
	pam_conf=$pam_conf_new
	conf_prefix="$PAM_client "
    elif $isdir ; then
	pam_conf=${pam_dir}/${PAM_client}
	conf_prefix=""
    else
	pam_conf=$pam_conf_sys
	conf_prefix="$PAM_client "
    fi

    # Deal with alternate root installs
    if [ -n "$PKG_INSTALL_ROOT" ]; then
	pam_conf=${PKG_INSTALL_ROOT}/${pam_conf}
    fi

    debug
    debug "" "Current Working Parameters:"
    debug "" "    config: [$config]"
    debug "" "    PAM conf file: [$pam_conf]"
    debug "" "    PAM client: [$PAM_client]"
    debug "" "    PAM type: [$PAM_type]"
    debug "" "    PAM module: [$PAM_module]"
    debug "" "    PAM module args: [$PAM_module_args]"
    debug "" "    Template client: [$template_client]"
    debug "" "    Default template client: [$default_template_client]"
    debug "" "    Configuration prefix: [$conf_prefix]"
    debug "" "    New PAM type: [$newpam]"
    debug

    status=0

    case $config in
	    true) config "$pam_conf" "$PAM_client" "$PAM_type" "$PAM_module" "$PAM_module_args" "$tag" "$template_client" "$conf_prefix" ;;
	    false) 
		if [ -z "$tag" ] && [ "$PAM_type" == "$_ALL_" ]; then
		    # special case when removing all the module types, we need to cleanup
		    # the tag too.  So, generate the tag so that we remove the tag goo.
		    tag="$PAM_client by $SUNRAY_SOFT"
		fi

		unconfig "$pam_conf" "$PAM_client" "$PAM_type" "$tag" ;;
	    list) list "$pam_conf" "$PAM_client" "$tag" ;;
	    add) add_this_module "$pam_conf" "$PAM_client" "$PAM_type" "$PAM_module" "$PAM_module_args" "$tag" "$flag" "$conf_prefix" ;;

	    none) usage ; exit 1 ;;
    esac

    # cleanup any schmutz left over
    cleanup

    exit $status
