/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

#include <nss_dbdefs.h>
#include <pwd.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include <auth_attr.h>
#include <deflt.h>
#include <priv.h>
#include <secdb.h>
#include <user_attr.h>
#include <sys/task.h>
#include <libintl.h>
#include <project.h>
#include <errno.h>
#include <alloca.h>

#include <bsm/adt.h>
#include <bsm/adt_event.h>	/* adt_get_auid() */

#include <security/pam_appl.h>
#include <security/pam_modules.h>
#include <security/pam_impl.h>

#define	PROJECT		"project="
#define	PROJSZ		(sizeof (PROJECT) - 1)

/*
 *	unix_cred - PAM auth modules must contain both pam_sm_authenticate
 *		and pam_sm_setcred.  Some other auth module is responsible
 *		for authentication (e.g., pam_unix_auth.so), this module
 *		only implements pam_sm_setcred so that the authentication
 *		can be separated without knowledge of the Solaris Unix style
 *		credential setting.
 *		Solaris Unix style credential setting includes initializing
 *		the audit characteristics if not already initialized and
 *		setting the user's default and limit privileges.
 */

/*
 *	unix_cred - pam_sm_authenticate
 *
 *	Returns	PAM_IGNORE.
 */

/*ARGSUSED*/
int
pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)
{
	return (PAM_IGNORE);
}

/*
 * Obtain a privilege set "keyname" from userattr; if none is present,
 * fall back to the default, "defname".
 */
static int
getset(char *keyname, char *defname, userattr_t *ua, priv_set_t **res,
    void *defp)
{
	char *str;
	priv_set_t *tmp;
	char *badp;
	int len;

	if ((ua == NULL || ua->attr == NULL ||
	    (str = kva_match(ua->attr, keyname)) == NULL) &&
	    (defp == NULL || (str = defread_r(defname, defp)) == NULL))
		return (0);

	len = strlen(str) + 1;
	badp = alloca(len);
	(void) memset(badp, '\0', len);
	do {
		const char *q, *endp;
		tmp = priv_str_to_set(str, ",", &endp);
		if (tmp == NULL) {
			if (endp == NULL)
				break;

			/* Now remove the bad privilege endp points to */
			q = strchr(endp, ',');
			if (q == NULL)
				q = endp + strlen(endp);

			if (*badp != '\0')
				(void) strlcat(badp, ",", len);
			/* Memset above guarantees NUL termination */
			/* LINTED */
			(void) strncat(badp, endp, q - endp);
			/* excise bad privilege; strtok ignores 2x sep */
			(void) memmove((void *)endp, q, strlen(q) + 1);
		}
	} while (tmp == NULL && *str != '\0');

	if (tmp == NULL) {
		syslog(LOG_AUTH|LOG_ERR,
		    "pam_setcred: can't parse privilege specification: %m\n");
		return (-1);
	} else if (*badp != '\0') {
		syslog(LOG_AUTH|LOG_DEBUG,
		    "pam_setcred: unrecognized privilege(s): %s\n", badp);
	}
	*res = tmp;
	return (0);
}

/*
 *	unix_cred - pam_sm_setcred
 *
 *	Entry flags = 	PAM_ESTABLISH_CRED, set up Solaris Unix cred.
 *			PAM_DELETE_CRED, NOP, return PAM_SUCCESS.
 *			PAM_REINITIALIZE_CRED, set up Solaris Unix cred,
 *				or merge the current context with the new
 *				user.
 *			PAM_REFRESH_CRED, set up Solaris Unix cred.
 *			PAM_SILENT, print no messages to user.
 *
 *	Returns	PAM_SUCCESS, if all successful.
 *		PAM_CRED_ERR, if unable to set credentials.
 *		PAM_USER_UNKNOWN, if PAM_USER not set, or unable to find
 *			user in databases.
 *		PAM_SYSTEM_ERR, if no valid flag, or unable to get/set
 *			user's audit state.
 */

int
pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv)
{
	int	i;
	int	debug = 0;
	uint_t	nowarn = flags & PAM_SILENT;
	int	ret = PAM_SUCCESS;
	char	*user;
	char	*auser;
	char	*rhost;
	char	*tty;
	au_id_t	auid;
	adt_session_data_t *ah;
	adt_termid_t	*termid = NULL;
	userattr_t	*ua;
	priv_set_t	*lim, *def, *tset;
	char		messages[PAM_MAX_NUM_MSG][PAM_MAX_MSG_SIZE];
	char		buf[PROJECT_BUFSZ];
	struct project	proj, *pproj;
	int		error;
	char		*projname;
	char		*kvs;
	struct passwd	pwd;
	char		pwbuf[NSS_BUFLEN_PASSWD];
	void		*defp;

	for (i = 0; i < argc; i++) {
		if (strcmp(argv[i], "debug") == 0)
			debug = 1;
		else if (strcmp(argv[i], "nowarn") == 0)
			nowarn |= 1;
	}

	if (debug)
		syslog(LOG_AUTH | LOG_DEBUG,
		    "pam_unix_cred: pam_sm_setcred(flags = %x, argc= %d)",
		    flags, argc);

	(void) pam_get_item(pamh, PAM_USER, (void **)&user);

	if (user == NULL || *user == '\0') {
		syslog(LOG_AUTH | LOG_ERR,
		    "pam_unix_cred: USER NULL or empty!\n");
		return (PAM_USER_UNKNOWN);
	}
	(void) pam_get_item(pamh, PAM_AUSER, (void **)&auser);
	(void) pam_get_item(pamh, PAM_RHOST, (void **)&rhost);
	(void) pam_get_item(pamh, PAM_TTY, (void **)&tty);
	if (debug)
		syslog(LOG_AUTH | LOG_DEBUG,
		    "pam_unix_cred: user = %s, auser = %s, rhost = %s, "
		    "tty = %s", user,
		    (auser == NULL) ? "NULL" : (*auser == '\0') ? "ZERO" :
		    auser,
		    (rhost == NULL) ? "NULL" : (*rhost == '\0') ? "ZERO" :
		    rhost,
		    (tty == NULL) ? "NULL" : (*tty == '\0') ? "ZERO" :
		    tty);

	/* validate flags */
	switch (flags & (PAM_ESTABLISH_CRED | PAM_DELETE_CRED |
	    PAM_REINITIALIZE_CRED | PAM_REFRESH_CRED)) {
	case 0:
		/* set default flag */
		flags |= PAM_ESTABLISH_CRED;
		break;
	case PAM_ESTABLISH_CRED:
	case PAM_REINITIALIZE_CRED:
	case PAM_REFRESH_CRED:
		break;
	case PAM_DELETE_CRED:
		return (PAM_SUCCESS);
	default:
		syslog(LOG_AUTH | LOG_ERR,
		    "pam_unix_cred: invalid flags %x", flags);
		return (PAM_SYSTEM_ERR);
	}

	/*
	 * if auditing on and process audit state not set,
	 * setup audit context for process.
	 */
	if (adt_start_session(&ah, NULL, ADT_USE_PROC_DATA) != 0) {
		syslog(LOG_AUTH | LOG_ERR,
		    "pam_unix_cred: cannot create start audit session %m");
		return (PAM_SYSTEM_ERR);
	}
	adt_get_auid(ah, &auid);
	if (debug) {
		int	auditstate;

		if (auditon(A_GETCOND, (caddr_t)&auditstate,
		    sizeof (auditstate)) != 0) {
			auditstate = AUC_DISABLED;
		}
		syslog(LOG_AUTH | LOG_DEBUG,
		    "pam_unix_cred: state = %d, auid = %d", auditstate,
		    auid);
	}
	if (getpwnam_r(user, &pwd, pwbuf, sizeof (pwbuf)) == NULL) {
		syslog(LOG_AUTH | LOG_ERR,
		    "pam_unix_cred: cannot get passwd entry for user = %s",
		    user);
		ret = PAM_USER_UNKNOWN;
		goto adt_done;
	}

	if ((auid == AU_NOAUDITID) &&
	    (flags & PAM_ESTABLISH_CRED)) {
		struct passwd	apwd;
		char	apwbuf[NSS_BUFLEN_PASSWD];

		errno = 0;
		if ((rhost == NULL || *rhost == '\0')) {
			if (adt_load_ttyname(tty, &termid) != 0) {
				if (errno != 0)
					syslog(LOG_AUTH | LOG_ERR,
					    "pam_unix_cred: cannot load "
					    "ttyname: %m.");
				else
					syslog(LOG_AUTH | LOG_ERR,
					    "pam_unix_cred: cannot load "
					    "ttyname.");
				ret = PAM_SYSTEM_ERR;
				goto adt_done;
			}
		} else {
			if (adt_load_hostname(rhost, &termid) != 0) {
				if (errno != 0)
					syslog(LOG_AUTH | LOG_ERR,
					    "pam_unix_cred: cannot load "
					    "hostname: %m.");
				else
					syslog(LOG_AUTH | LOG_ERR,
					    "pam_unix_cred: cannot load "
					    "hostname.");
				ret = PAM_SYSTEM_ERR;
				goto adt_done;
			}
		}
		if ((auser != NULL) && (*auser != '\0') &&
		    (getpwnam_r(auser, &apwd, apwbuf,
		    sizeof (apwbuf)) != NULL)) {
			/*
			 * set up the initial audit for user coming
			 * from another user
			 */
			if (adt_set_user(ah, apwd.pw_uid, apwd.pw_gid,
			    apwd.pw_uid, apwd.pw_gid, termid, ADT_NEW) != 0) {
				syslog(LOG_AUTH | LOG_ERR,
				    "pam_unix_cred: cannot set auser audit "
				    "%m");
				ret = PAM_SYSTEM_ERR;
				goto adt_done;
			}
			if (adt_set_user(ah, pwd.pw_uid, pwd.pw_gid,
			    pwd.pw_uid, pwd.pw_gid, NULL,
			    ADT_UPDATE) != 0) {
				syslog(LOG_AUTH | LOG_ERR,
				    "pam_unix_cred: cannot merge user audit "
				    "%m");
				ret = PAM_SYSTEM_ERR;
				goto adt_done;
			}
			if (debug) {
				syslog(LOG_AUTH | LOG_DEBUG,
				    "pam_unix_cred: new audit set for %d:%d",
				    apwd.pw_uid, pwd.pw_uid);
			}
		} else {
			/*
			 * No authenticated user or authenticated user is
			 * not a local user, no remote attribution, set
			 * up the initial audit as for direct user login
			 */
			if (adt_set_user(ah, pwd.pw_uid, pwd.pw_gid,
			    pwd.pw_uid, pwd.pw_gid, termid, ADT_NEW) != 0) {
				syslog(LOG_AUTH | LOG_ERR,
				    "pam_unix_cred: cannot set user audit %m");
				ret = PAM_SYSTEM_ERR;
				goto adt_done;
			}
		}
		if (adt_set_proc(ah) != 0) {
			syslog(LOG_AUTH | LOG_ERR,
			    "pam_unix_cred: cannot set process audit %m");
			ret = PAM_CRED_ERR;
			goto adt_done;
		}
		if (debug) {
			syslog(LOG_AUTH | LOG_DEBUG,
			    "pam_unix_cred: new audit set for %d",
			    pwd.pw_uid);
		}
	} else if ((auid != AU_NOAUDITID) &&
	    (flags & PAM_REINITIALIZE_CRED)) {
		if (adt_set_user(ah, pwd.pw_uid, pwd.pw_gid, pwd.pw_uid,
		    pwd.pw_gid, NULL, ADT_UPDATE) != 0) {
			syslog(LOG_AUTH | LOG_ERR,
			    "pam_unix_cred: cannot set user audit %m");
			ret = PAM_SYSTEM_ERR;
			goto adt_done;
		}
		if (adt_set_proc(ah) != 0) {
			syslog(LOG_AUTH | LOG_ERR,
			    "pam_unix_cred: cannot set process audit %m");
			ret = PAM_CRED_ERR;
			goto adt_done;
		}
		if (debug) {
			syslog(LOG_AUTH | LOG_DEBUG,
			    "pam_unix_cred: audit merged for %d:%d",
			    auid, pwd.pw_uid);
		}
	} else if (debug) {
		syslog(LOG_AUTH | LOG_DEBUG,
		    "pam_unix_cred: audit already set for %d", auid);
	}
adt_done:
	if (termid != NULL)
		free(termid);
	if (adt_end_session(ah) != 0) {
		syslog(LOG_AUTH | LOG_ERR,
		    "pam_unix_cred: unable to end audit session");
	}

	if (ret != PAM_SUCCESS)
		return (ret);

	/* Initialize the user's project */
	(void) pam_get_item(pamh, PAM_RESOURCE, (void **)&kvs);
	if (kvs != NULL) {
		char *tmp, *lasts, *tok;

		kvs = tmp = strdup(kvs);
		if (kvs == NULL)
			return (PAM_BUF_ERR);

		while ((tok = strtok_r(tmp, ";", &lasts)) != NULL) {
			if (strncmp(tok, PROJECT, PROJSZ) == 0) {
				projname = tok + PROJSZ;
				break;
			}
			tmp = NULL;
		}
	} else {
		projname = NULL;
	}

	if (projname == NULL || *projname == '\0') {
		pproj = getdefaultproj(user, &proj, (void *)&buf,
		    PROJECT_BUFSZ);
	} else {
		pproj = getprojbyname(projname, &proj, (void *)&buf,
		    PROJECT_BUFSZ);
	}
	/* projname points into kvs, so this is the first opportunity to free */
	if (kvs != NULL)
		free(kvs);
	if (pproj == NULL) {
		syslog(LOG_AUTH | LOG_ERR,
		    "pam_unix_cred: no default project for user %s", user);
		if (!nowarn) {
			(void) snprintf(messages[0], sizeof (messages[0]),
			    dgettext(TEXT_DOMAIN, "No default project!"));
			(void) __pam_display_msg(pamh, PAM_ERROR_MSG,
			    1, messages, NULL);
		}
		return (PAM_SYSTEM_ERR);
	}
	if ((error = setproject(proj.pj_name, user, TASK_NORMAL)) != 0) {
		kva_t *kv_array;

		switch (error) {
		case SETPROJ_ERR_TASK:
			if (errno == EAGAIN) {
				syslog(LOG_AUTH | LOG_ERR,
				    "pam_unix_cred: project \"%s\" resource "
				    "control limit has been reached",
				    proj.pj_name);
				(void) snprintf(messages[0],
				    sizeof (messages[0]), dgettext(
				    TEXT_DOMAIN,
				    "Resource control limit has been "
				    "reached"));
			} else {
				syslog(LOG_AUTH | LOG_ERR,
				    "pam_unix_cred: user %s could not join "
				    "project \"%s\": %m", user, proj.pj_name);
				(void) snprintf(messages[0],
				    sizeof (messages[0]), dgettext(
				    TEXT_DOMAIN,
				    "Could not join default project"));
			}
			if (!nowarn)
				(void) __pam_display_msg(pamh, PAM_ERROR_MSG, 1,
				    messages, NULL);
			break;
		case SETPROJ_ERR_POOL:
			(void) snprintf(messages[0], sizeof (messages[0]),
			    dgettext(TEXT_DOMAIN,
			    "Could not bind to resource pool"));
			switch (errno) {
			case EACCES:
				syslog(LOG_AUTH | LOG_ERR,
				    "pam_unix_cred: project \"%s\" could not "
				    "bind to resource pool: No resource pool "
				    "accepting default bindings exists",
				    proj.pj_name);
				(void) snprintf(messages[1],
				    sizeof (messages[1]),
				    dgettext(TEXT_DOMAIN,
				    "No resource pool accepting "
				    "default bindings exists"));
				break;
			case ESRCH:
				syslog(LOG_AUTH | LOG_ERR,
				    "pam_unix_cred: project \"%s\" could not "
				    "bind to resource pool: The resource pool "
				    "is unknown", proj.pj_name);
				(void) snprintf(messages[1],
				    sizeof (messages[1]),
				    dgettext(TEXT_DOMAIN,
				    "The specified resource pool "
				    "is unknown"));
				break;
			default:
				(void) snprintf(messages[1],
				    sizeof (messages[1]),
				    dgettext(TEXT_DOMAIN,
				    "Failure during pool binding"));
				syslog(LOG_AUTH | LOG_ERR,
				    "pam_unix_cred: project \"%s\" could not "
				    "bind to resource pool: %m", proj.pj_name);
			}
			if (!nowarn)
				(void) __pam_display_msg(pamh, PAM_ERROR_MSG,
				    2, messages, NULL);
			break;
		default:
			/*
			 * Resource control assignment failed.  Unlike
			 * newtask(1m), we treat this as an error.
			 */
			if (error < 0) {
				/*
				 * This isn't supposed to happen, but in
				 * case it does, this error message
				 * doesn't use error as an index, like
				 * the others might.
				 */
				syslog(LOG_AUTH | LOG_ERR,
				    "pam_unix_cred: unkwown error joining "
				    "project \"%s\" (%d)", proj.pj_name, error);
				(void) snprintf(messages[0],
				    sizeof (messages[0]),
				    dgettext(TEXT_DOMAIN,
				    "unkwown error joining project \"%s\""
				    " (%d)"), proj.pj_name, error);
			} else if ((kv_array = _str2kva(proj.pj_attr, KV_ASSIGN,
			    KV_DELIMITER)) != NULL) {
				syslog(LOG_AUTH | LOG_ERR,
				    "pam_unix_cred: %s resource control "
				    "assignment failed for project \"%s\"",
				    kv_array->data[error - 1].key,
				    proj.pj_name);
				(void) snprintf(messages[0],
				    sizeof (messages[0]),
				    dgettext(TEXT_DOMAIN,
				    "%s resource control assignment failed for "
				    "project \"%s\""),
				    kv_array->data[error - 1].key,
				    proj.pj_name);
				_kva_free(kv_array);
			} else {
				syslog(LOG_AUTH | LOG_ERR,
				    "pam_unix_cred: resource control "
				    "assignment failed for project \"%s\""
				    "attribute %d", proj.pj_name, error);
				(void) snprintf(messages[0],
				    sizeof (messages[0]),
				    dgettext(TEXT_DOMAIN,
				    "resource control assignment failed for "
				    "project \"%s\" attribute %d"),
				    proj.pj_name, error);
			}
			if (!nowarn)
				(void) __pam_display_msg(pamh, PAM_ERROR_MSG,
				    1, messages, NULL);
		}
		return (PAM_SYSTEM_ERR);
	}

	ua = getusernam(user);

	defp = defopen_r(AUTH_POLICY);

	tset = def = lim = NULL;

	if (getset(USERATTR_LIMPRIV_KW, DEF_LIMITPRIV, ua, &lim, defp) != 0 ||
	    getset(USERATTR_DFLTPRIV_KW, DEF_DFLTPRIV, ua, &def, defp) != 0) {
		ret = PAM_SYSTEM_ERR;
		goto out;
	}

	if (def == NULL) {
		def = priv_str_to_set("basic", ",", NULL);
		errno = 0;
		if ((pathconf("/", _PC_CHOWN_RESTRICTED) == -1) && (errno == 0))
			(void) priv_addset(def, PRIV_FILE_CHOWN_SELF);
	}
	/*
	 * Silently limit the privileges to those actually available
	 * in the current zone.
	 */
	tset = priv_allocset();
	if (tset == NULL) {
		ret = PAM_SYSTEM_ERR;
		goto out;
	}
	if (getppriv(PRIV_PERMITTED, tset) != 0) {
		ret = PAM_SYSTEM_ERR;
		goto out;
	}
	if (!priv_issubset(def, tset))
		priv_intersect(tset, def);
	/*
	 * We set privilege awareness here so that I gets copied to
	 * P & E when the final setuid(uid) happens.
	 */
	(void) setpflags(PRIV_AWARE, 1);
	if (setppriv(PRIV_SET, PRIV_INHERITABLE, def) != 0) {
		syslog(LOG_AUTH | LOG_ERR,
		    "pam_setcred: setppriv(defaultpriv) failed: %m");
		ret = PAM_CRED_ERR;
	}

	if (lim != NULL) {
		/*
		 * Silently limit the privileges to the limit set available.
		 */
		if (getppriv(PRIV_LIMIT, tset) != 0) {
			ret = PAM_SYSTEM_ERR;
			goto out;
		}
		if (!priv_issubset(lim, tset))
			priv_intersect(tset, lim);
		if (setppriv(PRIV_SET, PRIV_LIMIT, lim) != 0) {
			syslog(LOG_AUTH | LOG_ERR,
			    "pam_setcred: setppriv(limitpriv) failed: %m");
			ret = PAM_CRED_ERR;
			goto out;
		}
		/*
		 * In order not to surprise certain applications, we
		 * need to get rid of privilege awareness and thus we must
		 * set this flag which will cause a reset on set*uid().
		 */
		(void) setpflags(PRIV_AWARE_RESET, 1);
	}
	/*
	 * This may fail but we do not care as this will be reset later
	 * when the uids are set to their final values.
	 */
	(void) setpflags(PRIV_AWARE, 0);

out:
	if (defp != NULL)
		defclose_r(defp);

	if (ua != NULL)
		free_userattr(ua);
	if (lim != NULL)
		priv_freeset(lim);
	if (def != NULL)
		priv_freeset(def);
	if (tset != NULL)
		priv_freeset(tset);

	return (ret);
}