/*
 * 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 <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <syslog.h>
#include <netdb.h>
#include <malloc.h>
#include <unistd.h>
#include <errno.h>
#include <grp.h>
#include <security/pam_appl.h>
#include <security/pam_modules.h>
#include <security/pam_impl.h>

#define	ILLEGAL_COMBINATION "pam_list: illegal combination of options"

typedef enum {
	LIST_EXTERNAL_FILE,
	LIST_PLUS_CHECK,
	LIST_COMPAT_MODE
} pam_list_mode_t;

static const char *
string_mode_type(pam_list_mode_t op_mode, boolean_t allow)
{
	return ((op_mode == LIST_COMPAT_MODE) ? "compat" :
	    (allow ? "allow" : "deny"));
}

static void
log_illegal_combination(const char *s1, const char *s2)
{
	__pam_log(LOG_AUTH | LOG_ERR, ILLEGAL_COMBINATION
	    " %s and %s", s1, s2);
}

/*ARGSUSED*/
int
pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv)
{
	FILE	*fd;
	const char	*allowdeny_filename = PF_PATH;
	char	buf[BUFSIZ];
	char	hostname[MAXHOSTNAMELEN];
	char	*username = NULL;
	char	*grbuf = NULL;
	char	*bufp;
	char	*rhost;
	char	*limit;
	int	userok = 0;
	int	hostok = 0;
	int	i;
	int	allow_deny_test = 0;
	long	grbuflen = 0;
	boolean_t	debug = B_FALSE;
	boolean_t	allow = B_FALSE;
	boolean_t	matched = B_FALSE;
	boolean_t	check_user = B_TRUE;
	boolean_t	check_group = B_FALSE;
	boolean_t	check_host = B_FALSE;
	boolean_t	check_exact = B_FALSE;
	pam_list_mode_t	op_mode = LIST_PLUS_CHECK;

	// group reentrant interfaces limits
	if ((grbuflen = sysconf(_SC_GETGR_R_SIZE_MAX)) <= 0)
		return (PAM_BUF_ERR);

	for (i = 0; i < argc; ++i) {
		if (strncasecmp(argv[i], "debug", sizeof ("debug")) == 0) {
			debug = B_TRUE;
		} else if (strncasecmp(argv[i], "group",
		    sizeof ("group")) == 0) {
			check_group = B_TRUE;
		} else if (strncasecmp(argv[i], "user", sizeof ("user")) == 0) {
			check_user = B_TRUE;
		} else if (strncasecmp(argv[i], "nouser",
		    sizeof ("nouser")) == 0) {
			check_user = B_FALSE;
		} else if (strncasecmp(argv[i], "host", sizeof ("host")) == 0) {
			check_host = B_TRUE;
		} else if (strncasecmp(argv[i], "nohost",
		    sizeof ("nohost")) == 0) {
			check_host = B_FALSE;
		} else if (strncasecmp(argv[i], "user_host_exact",
		    sizeof ("user_host_exact")) == 0) {
			check_exact = B_TRUE;
		} else if (strcasecmp(argv[i], "compat") == 0) {
			if (op_mode == LIST_PLUS_CHECK) {
				op_mode = LIST_COMPAT_MODE;
			} else {
				log_illegal_combination("compat",
				    string_mode_type(op_mode, allow));
				return (PAM_SERVICE_ERR);
			}
		} else if (strncasecmp(argv[i], "allow=",
		    sizeof ("allow=") - 1) == 0) {
			if (op_mode == LIST_PLUS_CHECK) {
				allowdeny_filename = argv[i] +
				    sizeof ("allow=") - 1;
				allow = B_TRUE;
				op_mode = LIST_EXTERNAL_FILE;
				allow_deny_test++;
			} else {
				log_illegal_combination("allow",
				    string_mode_type(op_mode, allow));
				return (PAM_SERVICE_ERR);
			}
		} else if (strncasecmp(argv[i], "deny=",
		    sizeof ("deny=") - 1) == 0) {
			if (op_mode == LIST_PLUS_CHECK) {
				allowdeny_filename = argv[i] +
				    sizeof ("deny=") - 1;
				allow = B_FALSE;
				op_mode = LIST_EXTERNAL_FILE;
				allow_deny_test++;
			} else {
				log_illegal_combination("deny",
				    string_mode_type(op_mode, allow));
				return (PAM_SERVICE_ERR);
			}
		} else {
			__pam_log(LOG_AUTH | LOG_ERR,
			    "pam_list: illegal option %s", argv[i]);
			return (PAM_SERVICE_ERR);
		}
	}

	if (((check_user || check_group || check_host ||
	    check_exact) == B_FALSE) || (allow_deny_test > 1)) {
		__pam_log(LOG_AUTH | LOG_ERR, ILLEGAL_COMBINATION);
		return (PAM_SERVICE_ERR);
	}

	if ((op_mode == LIST_COMPAT_MODE) && (check_user == B_FALSE)) {
		log_illegal_combination("compat", "nouser");
		return (PAM_SERVICE_ERR);
	}

	if ((op_mode == LIST_COMPAT_MODE) && (check_group == B_TRUE)) {
		log_illegal_combination("compat", "group");
		return (PAM_SERVICE_ERR);
	}

	if (debug) {
		__pam_log(LOG_AUTH | LOG_DEBUG,
		    "pam_list: check_user = %d, check_host = %d,"
		    "check_exact = %d\n",
		    check_user, check_host, check_exact);

		__pam_log(LOG_AUTH | LOG_DEBUG,
		    "pam_list: auth_file: %s, %s\n", allowdeny_filename,
		    (op_mode == LIST_COMPAT_MODE) ? "compat mode" :
		    (allow ? "allow file" : "deny file"));
	}

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

	if ((check_user || check_group || check_exact) && ((username == NULL) ||
	    (*username == '\0'))) {
		__pam_log(LOG_AUTH | LOG_ERR,
		    "pam_list: username not supplied, critical error");
		return (PAM_USER_UNKNOWN);
	}

	(void) pam_get_item(pamh, PAM_RHOST, (void**)&rhost);

	if ((check_host || check_exact) && ((rhost == NULL) ||
	    (*rhost == '\0'))) {
		if (gethostname(hostname, MAXHOSTNAMELEN) == 0) {
			rhost = hostname;
		} else {
			__pam_log(LOG_AUTH | LOG_ERR,
			    "pam_list: error by gethostname - %m");
			return (PAM_SERVICE_ERR);
		}
	}

	if (debug) {
		__pam_log(LOG_AUTH | LOG_DEBUG,
		    "pam_list: pam_sm_acct_mgmt for (%s,%s,)",
		    (rhost != NULL) ? rhost : "", username);
	}

	if (strlen(allowdeny_filename) == 0) {
		__pam_log(LOG_AUTH | LOG_ERR,
		    "pam_list: file name not specified");
		return (PAM_SERVICE_ERR);
	}

	if ((fd = fopen(allowdeny_filename, "rF")) == NULL) {
		__pam_log(LOG_AUTH | LOG_ERR, "pam_list: fopen of %s: %s",
		    allowdeny_filename, strerror(errno));
		return (PAM_SERVICE_ERR);
	}

	if (check_group && ((grbuf = calloc(1, grbuflen)) == NULL)) {
		__pam_log(LOG_AUTH | LOG_ERR,
		    "pam_list: could not allocate memory for group");
		return (PAM_BUF_ERR);
	}

	while (fgets(buf, BUFSIZ, fd) != NULL) {
		/* lines longer than BUFSIZ-1 */
		if ((strlen(buf) == (BUFSIZ - 1)) &&
		    (buf[BUFSIZ - 2] != '\n')) {
			while ((fgetc(fd) != '\n') && (!feof(fd))) {
				continue;
			}
			__pam_log(LOG_AUTH | LOG_DEBUG,
			    "pam_list: long line in file,"
			    "more than %d chars, the rest ignored", BUFSIZ - 1);
		}

		/* remove unneeded colons if necessary */
		if ((limit = strpbrk(buf, ":\n")) != NULL) {
			*limit = '\0';
		}

		/* ignore free values */
		if (buf[0] == '\0') {
			continue;
		}

		bufp = buf;

		/* test for interesting lines = +/- in /etc/passwd */
		if (op_mode == LIST_COMPAT_MODE) {
			/* simple + matches all */
			if ((buf[0] == '+') && (buf[1] == '\0')) {
				matched = B_TRUE;
				allow = B_TRUE;
				break;
			}

			/* simple - is not defined */
			if ((buf[0] == '-') && (buf[1] == '\0')) {
				__pam_log(LOG_AUTH | LOG_ERR,
				    "pam_list: simple minus unknown, "
				    "illegal line in " PF_PATH);
				(void) fclose(fd);
				free(grbuf);
				return (PAM_SERVICE_ERR);
			}

			/* @ is not allowed on the first position */
			if (buf[0] == '@') {
				__pam_log(LOG_AUTH | LOG_ERR,
				    "pam_list: @ is not allowed on the first "
				    "position in " PF_PATH);
				(void) fclose(fd);
				free(grbuf);
				return (PAM_SERVICE_ERR);
			}

			/* -user or -@netgroup */
			if (buf[0] == '-') {
				allow = B_FALSE;
				bufp++;
			/* +user or +@netgroup */
			} else if (buf[0] == '+') {
				allow = B_TRUE;
				bufp++;
			/* user */
			} else {
				allow = B_TRUE;
			}
		} else if (op_mode == LIST_PLUS_CHECK) {
			if (((buf[0] != '+') && (buf[0] != '-')) ||
			    (buf[1] == '\0')) {
				continue;
			}

			if (buf[0] == '+') {
				allow = B_TRUE;
			} else {
				allow = B_FALSE;
			}
			bufp++;
		}

		/*
		 * if -> netgroup line
		 * else if -> group line
		 * else -> user line
		 */
		if ((bufp[0] == '@') && (bufp[1] != '\0')) {
			bufp++;

			if (check_exact) {
				if (innetgr(bufp, rhost, username,
				    NULL) == 1) {
					matched = B_TRUE;
					break;
				}
			} else {
				if (check_user) {
					userok = innetgr(bufp, NULL, username,
					    NULL);
				} else {
					userok = 1;
				}
				if (check_host) {
					hostok = innetgr(bufp, rhost, NULL,
					    NULL);
				} else {
					hostok = 1;
				}
				if (userok && hostok) {
					matched = B_TRUE;
					break;
				}
			}
		} else if ((bufp[0] == '%') && (bufp[1] != '\0')) {
			char	**member;
			struct	group grp;

			if (check_group == B_FALSE)
				continue;

			bufp++;

			if (getgrnam_r(bufp, &grp, grbuf, grbuflen) != NULL) {
				for (member = grp.gr_mem; *member != NULL;
				    member++) {
					if (strcmp(*member, username) == 0) {
						matched = B_TRUE;
						break;
					}
				}
			} else {
				__pam_log(LOG_AUTH | LOG_ERR,
				    "pam_list: %s is not a known group",
				    bufp);
			}
		} else {
			if (check_user) {
				if (strcmp(bufp, username) == 0) {
					matched = B_TRUE;
					break;
				}
			}
		}

		/*
		 * No match found in /etc/passwd yet.  For compat mode
		 * a failure to match should result in a return of
		 * PAM_PERM_DENIED which is achieved below if 'matched'
		 * is false and 'allow' is true.
		 */
		if (op_mode == LIST_COMPAT_MODE) {
			allow = B_TRUE;
		}
	}
	(void) fclose(fd);
	free(grbuf);

	if (debug) {
		__pam_log(LOG_AUTH | LOG_DEBUG,
		    "pam_list: %s for %s", matched ? "matched" : "no match",
		    allow ? "allow" : "deny");
	}

	if (matched) {
		return (allow ? PAM_SUCCESS : PAM_PERM_DENIED);
	}
	/*
	 * For compatibility with passwd_compat mode to prevent root access
	 * denied.
	 */
	if (op_mode == LIST_PLUS_CHECK) {
		return (PAM_IGNORE);
	}
	return (allow ? PAM_PERM_DENIED : PAM_SUCCESS);
}