/* acl.c - deal with ACLs in menu code
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif				/* HAVE_CONFIG_H */
#include <stdio.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/param.h>
#include <unistd.h>
#include <pwd.h>
#include "bbs.h"


/* The eaccess() and group_member() functions were taken from sh-utils-2.0 */

# if !defined (S_IXUGO)
#  define S_IXUGO 0111
# endif				/* S_IXUGO */
#if !defined (R_OK)
# define R_OK 4
# define W_OK 2
# define X_OK 1
# define F_OK 0
#endif				/* R_OK */

struct group_info {
	int n_groups;
	GETGROUPS_T *group;
};

#if HAVE_GETGROUPS

static void free_group_info(struct group_info *g)
{
	free(g->group);
	free(g);
}

static struct group_info *get_group_info(void)
{
	int n_groups;
	int n_group_slots;
	struct group_info *gi;
	GETGROUPS_T *group;

	/* getgroups () returns the number of elements that it was able to
	   place into the array.  We simply continue to call getgroups ()
	   until the number of elements placed into the array is smaller than
	   the physical size of the array. */

	group = NULL;
	n_groups = 0;
	n_group_slots = 0;
	while (n_groups == n_group_slots) {
		n_group_slots += 64;
		group = (GETGROUPS_T *) realloc(group,
						n_group_slots *
						sizeof(GETGROUPS_T));
		n_groups = getgroups(n_group_slots, group);
	}

	/* In case of error, the user loses. */
	if (n_groups < 0) {
		free(group);
		return NULL;
	}

	gi = (struct group_info *) malloc(sizeof(*gi));
	gi->n_groups = n_groups;
	gi->group = group;

	return gi;
}

#endif				/* not HAVE_GETGROUPS */

/* Return non-zero if GID is one that we have in our groups list.
   If there is no getgroups function, return non-zero if GID matches
   either of the current or effective group IDs.  */

int group_member(gid_t gid)
{
#ifndef HAVE_GETGROUPS
	return ((gid == getgid()) || (gid == getegid()));
#else
	int i;
	int found;
	struct group_info *gi;

	if (gid == getgid())
		return (1);
	if (gid == getegid())
		return (1);

	gi = get_group_info();
	if (gi == NULL)
		return 0;

	/* Search through the list looking for GID. */
	found = 0;
	for (i = 0; i < gi->n_groups; i++) {
		if (gid == gi->group[i]) {
			found = 1;
			break;
		}
	}

	free_group_info(gi);

	return found;
#endif				/* HAVE_GETGROUPS */
}


/* A wrapper for stat () which disallows pathnames that are empty strings. */
static int test_stat(char *path, struct stat *finfo)
{
	if (*path == '\0') {
		errno = ENOENT;
		return (-1);
	}
	return (stat(path, finfo));
}


/* Do the same thing access(2) does, but use the effective uid and gid,
   and don't make the mistake of telling root that any file is executable.
   But this loses when the containing filesystem is mounted e.g. read-only.  */
static int eaccess(char *path, int mode)
{
	struct stat st;
	static int euid = -1;

	if (test_stat(path, &st) < 0)
		return (-1);

	if (euid == -1)
		euid = geteuid();

	if (euid == 0) {
		/* Root can read or write any file. */
		if (mode != X_OK)
			return (0);

		/* Root can execute any file that has any one of the execute
		   bits set. */
		if (st.st_mode & S_IXUGO)
			return (0);
	}

	if (st.st_uid == euid)		    /* owner */
		mode <<= 6;
	else if (group_member(st.st_gid))
		mode <<= 3;

	if (st.st_mode & mode)
		return (0);

	return (-1);
}


/* Return 1 if adding file "file" to the menu "menu" would constitute an ACL
 * violation, 0 if it's OK. A pointer to a static buffer containing the
 * error message is stored in "*eptr" if it is not 0.
 *
 * Returns OK if any of:
 *
 *   + file being linked to is owned by same user as menu linking to it
 *     UNLESS file is in the UDB, LDB, Status, Friends or Logdir directories
 *     or it is the u2ulog file AND the menu owner isn't the sysop
 *   + file being linked to is world readable
 *   + file being linked to has a group-unwritable .acl file containing the
 *     menu owner's name
 *
 * Directories cannot have associated .acl files.
 */
int hook_acl_check(char *file, char *menu, char **eptr)
{
	struct passwd *z;
	char buf[1024];
	struct stat sb;
	int menu_uid;
	FILE *fptr;
	char *a;

	if (stat(menu, &sb)) {		    /* couldn't stat menu */
		a = "\035CR\035BERROR:\035b Failed to stat menu\035CA";
		if (eptr)
			*eptr = a;
		return (1);
	}
	menu_uid = sb.st_uid;

	z = getpwuid(menu_uid);		    /* get menu owner name */
	if (!z) {			    /* owner unknown - violation */
		a = "\035CR\035BERROR:\035b Menu owner UID unknown\035CA";
		if (eptr)
			*eptr = a;
		return (1);
	}

	if (stat(file, &sb)) {		    /* couldn't stat file */
		a = "\035CR\035BERROR:\035b Failed to stat file\035CA";
		if (eptr)
			*eptr = a;
		return (1);
	}

	if (eaccess(file, R_OK) != 0) {	    /* check file is readable */
		a = "\035CR\035BERROR:\035b File is not readable\035CA";
		if (eptr)
			*eptr = a;
		return (1);
	}

	if (sb.st_uid == menu_uid) {	    /* UIDs match */
		if (bbs_is_sysop(z->pw_name))
			return (0);	    /* sysop - OK */
		realpath(cf_str("udb"), buf);
		a = strrchr(buf, '/');	    /* check not UDB */
		if (a)
			*a = 0;
		if (strncmp(file, buf, strlen(buf)) == 0) {
			a = "\035CR\035BERROR:\035b Permission denied\035CA";
			if (eptr)
				*eptr = a;
			return (1);
		}
		realpath(cf_str("ldb"), buf);
		a = strrchr(buf, '/');	    /* check not LDB */
		if (a)
			*a = 0;
		if (strncmp(file, buf, strlen(buf)) == 0) {
			a = "\035CR\035BERROR:\035b Permission denied\035CA";
			if (eptr)
				*eptr = a;
			return (1);
		}
		realpath(cf_str("friends"), buf);
		a = strrchr(buf, '/');	    /* check not Friends */
		if (a)
			*a = 0;
		if (strncmp(file, buf, strlen(buf)) == 0) {
			a = "\035CR\035BERROR:\035b Permission denied\035CA";
			if (eptr)
				*eptr = a;
			return (1);
		}
		realpath(cf_str("logdir"), buf);	/* check not logdir */
		if (strncmp(file, buf, strlen(buf)) == 0) {
			a = "\035CR\035BERROR:\035b Permission denied\035CA";
			if (eptr)
				*eptr = a;
			return (1);
		}
		realpath(cf_str("status"), buf);	/* check not status */
		if (strncmp(file, buf, strlen(buf)) == 0) {
			a = "\035CR\035BERROR:\035b Permission denied\035CA";
			if (eptr)
				*eptr = a;
			return (1);
		}
		realpath(cf_str("u2ulog"), buf);	/* check not u2ulog */
		if (strncmp(file, buf, strlen(buf)) == 0) {
			a = "\035CR\035BERROR:\035b Permission denied\035CA";
			if (eptr)
				*eptr = a;
			return (1);
		}
		return (0);		    /* OK */
	}

	if (sb.st_mode & S_IROTH)
		return (0);		    /* world readable - OK */

	if (S_ISDIR(sb.st_mode)) {	    /* directory - no ACL file */
		a = "\035CR\035BERROR:\035b Directories cannot have ACLs\035CA";
		if (eptr)
			*eptr = a;
		return (1);
	}

	strncpy(buf, file, 1000);	    /* beware of overflows */
	buf[1000] = 0;
	strcat(buf, ".acl");

	if (stat(buf, &sb)) {		    /* no ACL file - violation */
		a = "\035CR\035BERROR:\035b ACL file not found\035CA";
		if (eptr)
			*eptr = a;
		return (1);
	}

	if (sb.st_mode & S_IWGRP) {	    /* ACL writable - violation */
		a = "\035CR\035BERROR:\035b ACL is group writable\035CA";
		if (eptr)
			*eptr = a;
		return (1);
	}

	if (sb.st_mode & S_IWOTH) {	    /* ACL writable - violation */
		a = "\035CR\035BERROR:\035b ACL is world writable\035CA";
		if (eptr)
			*eptr = a;
		return (1);
	}

	fptr = fopen(buf, "r");		    /* open ACL file for reading */
	if (!fptr) {			    /* failed to read; violation */
		a = "\035CR\035BERROR:\035b ACL file could not be opened\035CA";
		if (eptr)
			*eptr = a;
		return (1);
	}

	while ((!feof(fptr)) && (!ferror(fptr))) {
		buf[0] = 0;
		fgets(buf, sizeof(buf) - 1, fptr);	/* read line */
		buf[sizeof(buf) - 1] = 0;
		a = strchr(buf, '\n');	    /* chop \n */
		if (a)
			*a = 0;
		if (buf[0] == 0)
			continue;	    /* skip blank lines */
		a = strtok(buf, " \011");
		while (a) {		    /* many names per line, so split */
			if (a[0] == '#') {  /* stop at a comment */
				a = 0;
				memset(buf, 0, sizeof(buf));	/* KLUDGE ALERT */
				continue;
			}
			if (!strcmp(a, z->pw_name)) {	/* matched name */
				fclose(fptr);
				return (0); /* ...OK */
			}
			a = strtok(0, " \011");
		}
	}

	fclose(fptr);			    /* no match - violation */

	a = "\035CR\035BERROR:\035b permission not granted by ACL\035CA";
	if (eptr)
		*eptr = a;
	return (1);
}

/* EOF */
