/**********************************************************************
 *  group.c -- add user to group file
 *
 * Copyright 1994 by the Massachusetts Institute of Technology
 * For copying and distribution information, please see the file
 * <mit-copyright.h>.
 **********************************************************************/
#include <mit-copyright.h>

#include <AL/AL.h>
#include <stdio.h>
#include <stdlib.h>
#include <hesiod.h>
#include <errno.h>
#include <string.h>
#include <limits.h>
#include <grp.h>
#include <sys/param.h>

#ifdef BSD4_4
#include <paths.h>
#define PATH_GROUP	_PATH_GROUP
#else
#define PATH_GROUP	"/etc/group"
#endif
#define PATH_GROUP_LOCK	"/etc/gtmp"

struct hesgroup {
    char *name;
    gid_t gid;
    int infile;
};

/* pwd is a filled-in passwd structure for the user to be added.
 * groups is NULL or contains space for at least ngroups gids.
 * ngroups is the maximum number of groups (including the primary
 *   group) that the user should be in at the end.
 * Returns the number of groups added, filling in groups with
 *   the groups that were added.
 *
 * Effect: add pwd->pw_name to primary group pwd->pw_group and hesiod
 *   groups, leaving no more than ngroups groups in the passwd file.
 *   give priority to the primary group.  The gids of the added groups
 *   are recorded in groups (if non-NULL), and the number of added groups
 *   is returned.
 *
 * The lack of garbage collection in C makes it difficult to divide this
 * up into smaller abstractions. */
int PWaddGroups(struct passwd *pwd, gid_t *groups, int ngroups)
{
#ifdef NO_HESIOD
    return 0;
#else
    char **hesvec, *buf = NULL, *p;
    char numbuf[16];
    FILE *groupfile, *tempfile;
    int namelen = strlen(pwd->pw_name), bufsize = 0, i;
    int hesiod_count = 0, infile_count = 0, added_count = 0;
    int primary_known = 0, ceiling, do_add;
    gid_t gid;
    struct hesgroup primary, *hesgroups;
    sigset_t mask;

    hesgroups = malloc(ngroups * sizeof(struct hesgroup));
    if (!hesgroups) {
	fclose(groupfile);
	return -1;
    }
    if (ALbeginFileUpdate(PATH_GROUP, PATH_GROUP_LOCK, &groupfile,
			  &tempfile, &mask) < 0) {
	free(hesgroups);
	return -1;
    }

    /* Find name of primary group. */
    sprintf(numbuf, "%ld", (long) pwd->pw_gid);
    hesvec = hes_resolve(numbuf, "gid");
    if (hesvec && *hesvec) {
	p = strchr(*hesvec, ':');
	if (p) {
	    *p = 0;
	    primary.name = *hesvec;
	    primary.gid = pwd->pw_gid;
	    primary.infile = 0;
	    primary_known = 1;
	}
	for (hesvec++; *hesvec; hesvec++)
	    free(*hesvec);
    }

    /* Parse up to ngroups hesiod groups from the group list. */
    hesvec = hes_resolve(pwd->pw_name, "grplist");
    if (hesvec && *hesvec) {
	p = *hesvec;
	while (hesiod_count < ngroups) {
	    hesgroups[hesiod_count].name = p;
	    p = strchr(p, ':');
	    if (p) {
		*p = 0;
		gid = atoi(p + 1);
		for (i = 0; i < hesiod_count; i++) {
		    if (hesgroups[i].gid == gid)
			break;
		}
		if (i < hesiod_count || (primary_known && primary.gid == gid))
		    continue;
		hesgroups[hesiod_count].gid = gid;
		hesgroups[hesiod_count++].infile = 0;
		p = strchr(p + 1, ':');
	    }
	    if (p)
		p++;
	    else
		break;
	}
    }

    /* Count groups in the file, and mark hesiod/primary groups in the file. */
    while (ALgetLine(groupfile, &buf, &bufsize)) {
	p = strchr(buf, ':');
	if (p)
	    p = strchr(p + 1, ':');
	if (p) {
	    gid = atoi(p + 1);
	    p = strchr(p + 1, ':');
	}
	while (p) {
	    if (strncmp(p + 1, pwd->pw_name, namelen) == 0 &&
		(p[namelen + 1] == ',' || p[namelen + 1] == 0)) {
		infile_count++;
		if (primary_known && gid == primary.gid) {
		    primary.infile = 1;
		} else {
		    for (i = 0; i < hesiod_count; i++) {
			if (gid == hesgroups[i].gid)
			    hesgroups[i].infile = 1;
		    }
		}
		break;
	    }
	    p = strchr(p + 1, ',');
	}
    }

    /* Copy existing group lines, adding the user if necessary.  Be sure to
     * leave room for the primary group if it's known and not in the file, and
     * be sure to record groups we add. */
    ceiling = (primary_known && !primary.infile) ? ngroups - 1 : ngroups;
    rewind(groupfile);
    while (ALgetLine(groupfile, &buf, &bufsize)) {
	do_add = 0;

	/* First, find the gid. */
	p = strchr(buf, ':');
	if (p)
	    p = strchr(p + 1, ':');
	if (p) {
	    gid = atoi(p + 1);

	    /* Now decide whether to add the user. */
	    if (primary_known && !primary.infile && primary.gid == gid &&
		infile_count < ngroups) {
		do_add = 1;
		primary.infile = 1;
		ceiling = ngroups;
	    } else if (infile_count < ceiling) {
		for (i = 0; i < hesiod_count; i++) {
		    if (!hesgroups[i].infile && gid == hesgroups[i].gid) {
			do_add = 1;
			hesgroups[i].infile = 1;
		    }
		}
	    }
	    if (do_add) {
		infile_count++;
		if (groups)
		    groups[added_count++] = gid;
	    }
	}
	
	/* Now copy the line, adding the user if desired. */
	fputs(buf, tempfile);
	if (do_add)
	    fprintf(tempfile, ",%s", pwd->pw_name);
	putc('\n', tempfile);
    }
    
    /* Add primary group, if it's not in the file and we have room. */
    if (infile_count < ngroups && primary_known && !primary.infile) {
	fprintf(tempfile, "%s:*:%ld:%s\n", primary.name,
		(long) primary.gid, pwd->pw_name);
	infile_count++;
	if (groups)
	    groups[added_count++] = primary.gid;
    }

    /* Add hesiod groups not in the file, as long as we have room. */
    for (i = 0; i < hesiod_count; i++) {
	if (infile_count < ngroups && !hesgroups[i].infile) {
	    fprintf(tempfile, "%s:*:%ld:%s\n", hesgroups[i].name,
		    (long) hesgroups[i].gid, pwd->pw_name);
	    infile_count++;
	    if (groups)
		groups[added_count++] = hesgroups[i].gid;
	}
    }
    
    /* Clean up allocated memory. */
    while (hesvec && *hesvec) {
	free(*hesvec);
	hesvec++;
    }
    if (primary_known)
	free(primary.name);
    free(hesgroups);
    if (buf)
	free(buf);

    if (ALfinishFileUpdate(PATH_GROUP, PATH_GROUP_LOCK, groupfile,
			   tempfile, &mask, 0) < 0)
	return -1;

    return added_count;
#endif
}

/* Remove pwd->pw_name from the lines whose gids are listed in groups. */
int PWremoveGroups(struct passwd *pwd, gid_t *groups, int ngroups)
{
    FILE *groupfile, *tempfile;
    int i, namelen = strlen(pwd->pw_name), bufsize = 0, group_empty = 0;
    char *buf = NULL, *p;
    gid_t gid;
    sigset_t mask;

    if (ALbeginFileUpdate(PATH_GROUP, PATH_GROUP_LOCK, &groupfile,
			  &tempfile, &mask) < 0)
	return -1;

    /* Copy existing group lines, pruning where appropriate. */
    while (ALgetLine(groupfile, &buf, &bufsize)) {
	group_empty = 0;
	/* Remove pwd->pw_name from the line if appropriate. */
	p = strchr(buf, ':');
	if (p)
	    p = strchr(p + 1, ':');
	if (p) {
	    gid = atoi(p + 1);
	    for (i = 0; i < ngroups; i++) {
		if (groups[i] == gid)
		    break;
	    }
	    if (i < ngroups) {
		/* Remove the user from this line. */
		p = strchr(p + 1, ':');
		while (p) {
		    if (strncmp(p + 1, pwd->pw_name, namelen) == 0) {
			if (p[1 + namelen] == ',' ) {
			    memmove(p + 1, p + 1 + namelen + 1,
				    strlen(p + 1 + namelen + 1) + 1);
			    break;
			} else if (p[1 + namelen] == 0) {
			    if (*p == ',')
				*p = 0;
			    else
				group_empty = 1;
			    break;
			}
		    }
		    p = strchr(p + 1, ',');
		}
	    }
	}

	/* Now copy the line. */
	if (!group_empty) {
	    fputs(buf, tempfile);
	    putc('\n', tempfile);
	}
    }

    if (buf)
	free(buf);
    return ALfinishFileUpdate(PATH_GROUP, PATH_GROUP_LOCK, groupfile,
			      tempfile, &mask, 0);
}

long
ALaddToGroupsFile(ALsession session)
{
    int count;

    session->ngroups = 0;
    count = PWaddGroups(&session->pwd, session->groups, ALmaxGroups);
    if (count < 0)
	ALreturnError(session, ALerrUpdateFail, "group update failed");
    session->ngroups = count;
    return 0;
}

long
ALremoveFromGroupsFile(ALsession session)
{
    if (PWremoveGroups(&session->pwd, session->groups, session->ngroups) < 0)
	ALreturnError(session, ALerrUpdateFail, "group update failed");
    return 0;
}

