/* update-passwd - Safely update /etc/passwd, /etc/shadow and /etc/group
 * Copyright 1999-2002 Wichert Akkerman <wichert@deephackmode.org>
 * Copyright 2002, 2003, 2004 Colin Watson <cjwatson@debian.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of version 2 of the GNU General Public License as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA. 
 *
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <limits.h>
#include <errno.h>
#include <getopt.h>
#include <assert.h>
#include <paths.h>
#include <pwd.h>
#include <shadow.h>
#include <grp.h>
#include <stdarg.h>
#include <ctype.h>

#ifdef HAVE_DEBCONF
#include <cdebconf/debconfclient.h>
#endif

#ifdef HAVE_LIBSELINUX
#include <selinux/selinux.h>
#include <selinux/label.h>
#endif

#define DEFAULT_PASSWD_MASTER	"/usr/share/base-passwd/passwd.master"
#define DEFAULT_GROUP_MASTER	"/usr/share/base-passwd/group.master"
#define DEFAULT_PASSWD_SYSTEM	"/etc/passwd"
#define DEFAULT_SHADOW_SYSTEM	_PATH_SHADOW
#define DEFAULT_GROUP_SYSTEM	"/etc/group"

#define DEFAULT_DEBCONF_DOMAIN	"system"

#define	WRITE_EXTENSION		".upwd-write"
#define	BACKUP_EXTENSION	".org"


#define FL_KEEPHOME	0x0001
#define FL_KEEPSHELL	0x0002
#define FL_KEEPGECOS	0x0004
#define FL_KEEPALL	0x000f

#define FL_NOAUTOREMOVE	0x0010
#define FL_NOAUTOADD	0x0020

/* This structure is actually used for both users and groups
 * we probably should split that someday.
 */
struct _info {
    uid_t	id;
    unsigned	flags;
};

const struct _info specialusers[] = {
    {  0, (FL_KEEPALL|FL_NOAUTOREMOVE)			},  /* root	*/
    { 11, (FL_KEEPHOME|FL_NOAUTOADD|FL_NOAUTOREMOVE)	},  /* ftp	*/
    { 30, (FL_KEEPHOME|FL_NOAUTOADD|FL_NOAUTOREMOVE)	},  /* majordom	*/
    { 31, (FL_NOAUTOREMOVE)				},  /* postgres */
    { 33, (FL_KEEPHOME)					},  /* www-data	*/
    { 35, (FL_KEEPALL|FL_NOAUTOADD|FL_NOAUTOREMOVE)	},  /* dos	*/
    { 36, (FL_KEEPALL|FL_NOAUTOADD|FL_NOAUTOREMOVE)	},  /* msql	*/
    { 37, (FL_NOAUTOREMOVE)				},  /* operator */
    { 41, (FL_KEEPHOME|FL_NOAUTOREMOVE)			},  /* gnats	*/
    { 70, (FL_NOAUTOREMOVE)				},  /* alias	*/
    { 71, (FL_KEEPALL|FL_NOAUTOADD|FL_NOAUTOREMOVE)	},  /* qmaild	*/
    { 72, (FL_KEEPALL|FL_NOAUTOADD|FL_NOAUTOREMOVE)	},  /* qmails	*/
    { 73, (FL_KEEPALL|FL_NOAUTOADD|FL_NOAUTOREMOVE)	},  /* qmailr	*/
    { 74, (FL_KEEPALL|FL_NOAUTOADD|FL_NOAUTOREMOVE)	},  /* qmailq	*/
    { 75, (FL_KEEPALL|FL_NOAUTOADD|FL_NOAUTOREMOVE) 	},  /* qmaill	*/
    { 76, (FL_KEEPALL|FL_NOAUTOADD|FL_NOAUTOREMOVE)	},  /* qmailp	*/
    { 0, 0}
};


const struct _info specialgroups[] = {
    {  0, (FL_KEEPALL|FL_NOAUTOREMOVE)			},  /* root	*/
    { 11, (FL_NOAUTOADD|FL_NOAUTOREMOVE)		},  /* ftp	*/
    { 31, (FL_NOAUTOADD|FL_NOAUTOREMOVE)		},  /* majordom	*/
    { 32, (FL_NOAUTOREMOVE)				},  /* postgres */
    { 35, (FL_NOAUTOADD|FL_NOAUTOREMOVE)		},  /* dos	*/
    { 36, (FL_NOAUTOADD|FL_NOAUTOREMOVE)		},  /* msql	*/
    { 41, (FL_NOAUTOREMOVE)				},  /* gnats	*/
    { 70, (FL_NOAUTOREMOVE)				},  /* qmail	*/
    { 0, 0}
};

struct _node {
    union {
	struct passwd	pw;
	struct spwd	sp;
	struct group	gr;
    } d;
    enum {
	t_passwd,
	t_shadow,
	t_group,
	t_error
    } t;
    const char*		name;
    uid_t		id;
    struct _node*	next;
    struct _node*	prev;
    struct _node*	last;
};

const char*	master_passwd	= DEFAULT_PASSWD_MASTER;
const char*	master_group	= DEFAULT_GROUP_MASTER;
const char*	sys_passwd	= DEFAULT_PASSWD_SYSTEM;
const char*	sys_shadow	= DEFAULT_SHADOW_SYSTEM;
const char*	sys_group	= DEFAULT_GROUP_SYSTEM;

struct _node*	master_accounts	= NULL;
struct _node*	master_groups	= NULL;
struct _node*	system_accounts	= NULL;
struct _node*	system_shadow	= NULL;
struct _node*	system_groups	= NULL;

int		opt_dryrun	= 0;
int		opt_verbose	= 0;
int		opt_nolock	= 0;
int		opt_sanity	= 0;

int		flag_dirty	= 0;
int		flag_debconf	= 0;

const char*	user_domain	= DEFAULT_DEBCONF_DOMAIN;
const char*	group_domain	= DEFAULT_DEBCONF_DOMAIN;

#ifdef HAVE_DEBCONF
struct debconfclient*	debconf	= NULL;
#endif

#ifdef HAVE_LIBSELINUX
/* SELinux support */

static struct selabel_handle* selabel_handle;

static int selinux_setup(void) {

    if (!is_selinux_enabled()) {
        return 0;
    }

    /* Because this process is short lived, don't bother with selabel_close
     * it isn't worth the additional complexity
     */
    selabel_handle = selabel_open(SELABEL_CTX_FILE, NULL, 0);
    if (selabel_handle == NULL) {
        const char * const reason = strerror(errno);

        if (security_getenforce() != 0) {
            fprintf(stderr, "SELinux: enabled but selabel_open failed: %s\n", reason);
            return 1;
        } else {
            fprintf(stderr, "SELinux: enabled but in permissive mode, selabel_open failed: %s\n", reason);
        }
    }

    return 0;
}

static int selinux_prepare_create_file(const char *path) {
    char * ctx = NULL;
    int    r   = 0;

    if (selabel_handle == NULL) {
        return 0;
    }

    if (selabel_lookup_raw(selabel_handle, &ctx, path, S_IFREG) != 0) {
        if (errno == ENOENT) {
            return 0;
        }

        const char * const reason = strerror(errno);

        if (security_getenforce() != 0) {
            fprintf(stderr, "SELinux: unable to lookup label for %s: %s\n", path, reason);
            return 1;
        } else {
            fprintf(stderr, "SELinux: unable to lookup label for %s: %s\n", path, reason);
            fprintf(stderr, "SELinux: ignoring error as in permissive mode, but a relabel is recommended.\n");
            return 0;
        }
    }

    r = setfscreatecon_raw(ctx);

    freecon(ctx);

    return r;
}

static void selinux_after_create_file(void) {
    if (selabel_handle != NULL) {
        int saved_errno = errno;
        setfscreatecon_raw(NULL);
        errno = saved_errno;
    }
}
#else
/* No SELinux support, provide stubs */

static int selinux_setup(void) {
    return 0;
}

static int selinux_prepare_create_file(const char *path) {
    (void)path;

    return 0;
}

static void selinux_after_create_file(void) {
    return;
}

#endif

#ifdef HAVE_DEBCONF
/* Abort the program if talking to debconf fails.  Use ret exactly once. */
#define DEBCONF_CHECK(ret)					\
    do {							\
	if ((ret)!=0) {						\
	    fprintf(stderr, "Debconf interaction failed\n");	\
	    exit(1);						\
	}							\
    } while (0)

/* Wrapper macros around the debconfclient interface that check the return
 * status and use the global debconf client.  The mechanics of asking the
 * question and retrieving the answer are handled by the ask_debconf
 * function below. */
#define DEBCONF_REGISTER(template, question) \
    DEBCONF_CHECK(debconf_register(debconf, (template), (question)))
#define DEBCONF_SUBST(question, var, value) \
    DEBCONF_CHECK(debconf_subst(debconf, (question), (var), (value)))
#else
#define DEBCONF_REGISTER(template, question)
#define DEBCONF_SUBST(question, var, value)
#endif


/* malloc() with out-of-memory checking.
 */
void* xmalloc(size_t n) {
    void *p=malloc(n);
    if (p==0 && n==0) {
	p=malloc((size_t)1);
	if (p==0) {
	    fprintf(stderr, "Out of memory!\n");
	    exit(1);
	}
    }
    return p;
}

/* Copy a string with out-of-memory checking.
 */
char* xstrdup(const char *string) {
    if (!string)
	return NULL;
    return strcpy(xmalloc(strlen(string) + 1), string);
}

/* asprintf() with out-of-memory checking.  Also fail if formatting fails so
 * that the caller doesn't have to check any error return.
 */
__attribute__((format (printf, 1, 2)))
char* xasprintf(const char* fmt, ...) {
    va_list	args;
    int		ret;
    char*	result;

    va_start(args, fmt);
    ret=vasprintf(&result, fmt, args);
    va_end(args);
    if (ret<0) {
	fprintf(stderr, "Formatting string failed: %s\n", strerror(errno));
	exit(1);
    }
    return result;
}

/* Create an empty list-entry
 */
struct _node* create_node(void) {
    struct _node*	newnode;

    newnode=(struct _node*)xmalloc(sizeof(struct _node));
    newnode->name=0;
    newnode->id=0;
    newnode->next=NULL;
    newnode->prev=NULL;
    newnode->last=NULL;
    newnode->t=t_error;

    return newnode;
}


void copy_passwd(struct _node* newnode, const struct passwd* pw) {
    newnode->d.pw=*pw;
    newnode->d.pw.pw_name=xstrdup(pw->pw_name);
    newnode->d.pw.pw_passwd=xstrdup(pw->pw_passwd);
    newnode->d.pw.pw_gecos=xstrdup(pw->pw_gecos);
    newnode->d.pw.pw_dir=xstrdup(pw->pw_dir);
    newnode->d.pw.pw_shell=xstrdup(pw->pw_shell);
}


void copy_shadow(struct _node* newnode, const struct spwd* sp) {
    newnode->d.sp=*sp;
    newnode->d.sp.sp_namp=xstrdup(sp->sp_namp);
    newnode->d.sp.sp_pwdp=xstrdup(sp->sp_pwdp);
}


void copy_group(struct _node* newnode, const struct group* gr) {
    int		memcount, mem;

    newnode->d.gr=*gr;
    newnode->d.gr.gr_name=xstrdup(gr->gr_name);
    newnode->d.gr.gr_passwd=xstrdup(gr->gr_passwd);

    for (memcount=0; gr->gr_mem[memcount]; ++memcount)
	;
    newnode->d.gr.gr_mem=xmalloc((memcount+1) * sizeof(char*));
    for (mem=0; mem<memcount; ++mem)
	newnode->d.gr.gr_mem[mem]=xstrdup(gr->gr_mem[mem]);
    newnode->d.gr.gr_mem[memcount]=NULL;
}

/* Make a copy of a list-entry
 */
struct _node* copy_node(const struct _node* node) {
    struct _node*	newnode;

    newnode=create_node();
    newnode->id=node->id;
    newnode->name=node->name;
    newnode->t=node->t;

    switch (newnode->t) {
	case t_passwd:
	    copy_passwd(newnode, &node->d.pw);
	    break;
	case t_shadow:
	    copy_shadow(newnode, &node->d.sp);
	    break;
	case t_group:
	    copy_group(newnode, &node->d.gr);
	    break;
	default:
	    fprintf(stderr, "Internal error: unexpected entrytype %d\n", newnode->t);
	    exit(1);
    }

    return newnode;
}


/* Add a new item to a list
 */
void add_node(struct _node** head, struct _node* node, int new_entry) {
    node->prev=NULL;
    node->next=NULL;

    if (*head==NULL) {
	*head=node;
	node->last=node;
	return;
    }

    if (new_entry) {
	/* Make sure NIS compat entries stay at the end when adding new
	 * entries.
	 */
	struct _node*	walk;
	for (walk=*head; walk; walk=walk->next) {
	    if (strcmp(walk->name, "+")==0)
		break;
	}
	/* Was there a "+" entry at all?
	 */
	if (walk) {
	    node->prev=walk->prev;
	    node->next=walk;
	    if (walk->prev)
		walk->prev->next=node;
	    walk->prev=node;
	    if (walk==*head) {
		node->last=(*head)->last;
		*head=node;
	    }
	    return;
	}
	/* Otherwise fall through and add as normal.
	 */
    }

    (*head)->last->next=node;
    node->prev=(*head)->last;
    (*head)->last=node;
}


/* Remove an item from a list
 */
void remove_node(struct _node** head, struct _node* node) {
    if (node==*head) {
	if (node->next) {
	    node->next->last=(*head)->last;
	    node->next->prev=NULL;
	}
	*head=node->next;
    } else {
	if (node==(*head)->last)
	    (*head)->last=node->prev;
	if (node->prev)
	    node->prev->next=node->next;
	if (node->next)
	    node->next->prev=node->prev;
    }

    node->prev=NULL;
    node->next=NULL;
}


/* Locate an entry with a specific name in the list
 */
struct _node* find_by_name(struct _node* head, const char* name) {
    while (head) {
	if (strcmp(name, head->name)==0)
	    return head;
	head=head->next;
    }

    return NULL;
}


/* Look for an entry in a list, using the name of _entry as the
 * searchkey.
 */
struct _node* find_by_named_entry(struct _node* head, const struct _node* entry) {
    while (head) {
	if (strcmp(entry->name, head->name)==0)
	    return head;
	head=head->next;
    }

    return NULL;
}


/* Look for an entry in a list, using the id of _entry as the
 * searchkey.
 */
struct _node* find_by_id(struct _node* head, uid_t id) {
    while (head) {
	if (id==head->id)
	    return head;
	head=head->next;
    }

    return NULL;
}


/* Function to scan the list of special users or groups to see if a an
 * entry has a certain flag set.
 */
int scan_infos(const struct _info *lst, uid_t id, unsigned flag) {
    const struct _info*	walk;
    for (walk=lst; !((walk->id==0) && (walk->flags==0)); walk++)
	if (walk->id==id)
	    return ((walk->flags&flag)!=0);
    return 0;
}

/* Just for our convenience */
int keephome(const struct _info* lst, uid_t id) {
    return scan_infos(lst, id, FL_KEEPHOME); }
int keepshell(const struct _info* lst, uid_t id) {
    return scan_infos(lst, id, FL_KEEPSHELL); }
int keepgecos(const struct _info* lst, uid_t id) {
    return scan_infos(lst, id, FL_KEEPGECOS); }
int noautoremove(const struct _info* lst, uid_t id) {
    return scan_infos(lst, id, FL_NOAUTOREMOVE); }
int noautoadd(const struct _info* lst, uid_t id) {
    return scan_infos(lst, id, FL_NOAUTOADD); }

/* Function to read passwd database */
int read_passwd(struct _node** list, const char* file) {
    FILE*		input;
    struct _node*	node;
    struct passwd*	result;

    if (opt_verbose>2)
	printf("Reading passwd from %s\n", file);

    if ((input=fopen(file, "r"))==NULL) {
	fprintf(stderr, "Error opening passwd file %s: %s\n", file, strerror(errno));
	return 1;
    }

    while ((result=fgetpwent(input))!=NULL) {
	node=create_node();
	copy_passwd(node, result);
	node->t=t_passwd;
	node->name=node->d.pw.pw_name;
	if (!node->name)
	    break;
	if (node->name[0]=='+')
	    node->id=0;
	else
	    node->id=node->d.pw.pw_uid;
	add_node(list, node, 0);
    }

    if ((result==NULL) && (errno!=ENOENT)) {
	fprintf(stderr, "Error reading passwd file %s: %s\n", file, strerror(errno));
	return 2;
    }

    fclose(input);

    return 0;
}


/* Function to read group database */
int read_group(struct _node** list, const char* file) {
    FILE*		input;
    struct _node*	node;
    struct group*	result;

    if (opt_verbose>2)
	printf("Reading group from %s\n", file);

    if ((input=fopen(file, "r"))==NULL) {
	fprintf(stderr, "Error opening group file %s: %s\n", file, strerror(errno));
	return 1;
    }

    while ((result=fgetgrent(input))!=NULL) {
	node=create_node();
	copy_group(node, result);
	node->t=t_group;
	node->name=node->d.gr.gr_name;
	if (!node->name)
	    break;
	if (node->name[0]=='+')
	    node->id=0;
	else
	    node->id=node->d.gr.gr_gid;
	add_node(list, node, 0);
    }

    if ((result==NULL) && (errno!=ENOENT)) {
	fprintf(stderr, "Error reading group file %s: %s\n", file, strerror(errno));
	return 2;
    }

    fclose(input);

    return 0;
}


/* Function to read shadow database */
int read_shadow(struct _node** list, const char* file) {
    FILE*		input;
    struct _node*	node;
    struct spwd*	result;

    if (opt_verbose>2)
	printf("Reading shadow from %s\n", file);

    if ((input=fopen(file, "r"))==NULL) {
	if (errno!=ENOENT)
	    fprintf(stderr, "Error opening shadow file %s: %s\n", file, strerror(errno));
	return 1;
    }

    while ((result=fgetspent(input))!=NULL) {
	node=create_node();
	copy_shadow(node, result);
	node->t=t_shadow;
	node->id=0;
	node->name=node->d.sp.sp_namp;
	if (!node->name)
	    break;
	add_node(list, node, 0);
    }

    if ((result==NULL) && (errno!=ENOENT)) {
	fprintf(stderr, "Error reading shadow file %s: %s\n", file, strerror(errno));
	return 2;
    }

    fclose(input);

    return 0;
}


/* Small helper functions to safely print strings that might be NULL.
 */
const char* safestr(const char* str) {
    if (str==NULL)
	return "";
    else
	return str;
}


/* Implement our own putpwent(3). The version in GNU libc is stupid enough
 * to not recognize NIS compat entries and will happily turn an entry like
 * this:
 *
 *    +@staff::::::
 *
 * into this:
 *
 *    +@staff::0:0:::
 *
 */

int fputpwent(const struct passwd *passwd, FILE * f) {
    int res;

    assert(passwd!=NULL);
    assert(f!=NULL);

    if (passwd->pw_name[0]=='+')
	res=fprintf(f, "%s:%s:::%s:%s:%s\n",
		safestr(passwd->pw_name),
		safestr(passwd->pw_passwd),
		safestr(passwd->pw_gecos),
		safestr(passwd->pw_dir),
		safestr(passwd->pw_shell));
    else
	res=fprintf(f, "%s:%s:%u:%u:%s:%s:%s\n",
		safestr(passwd->pw_name),
		safestr(passwd->pw_passwd),
		passwd->pw_uid, passwd->pw_gid,
		safestr(passwd->pw_gecos),
		safestr(passwd->pw_dir),
		safestr(passwd->pw_shell));

    if (res<0)
	return -1;

    return 0;
}


/* Simple function to print usage information */
void usage(void) {
    printf(
	"Usage: update-passwd [OPTION]...\n"
	"\n"
	"  -p, --passwd-master=file  Use file as the master account list\n"
	"  -g, --group-master=file   Use file as the master group list\n"
	"  -P, --passwd=file         Use file as the system passwd file\n"
	"  -S, --shadow=file         Use file as the system shadow file\n"
	"  -G, --group=file          Use file as the system group file\n"
	"  -s, --sanity-check        Only perform sanity-checks\n"
	"  -v, --verbose             Show details about what we are doing (recommended)\n"
	"  -n, --dry-run             Just say what we would do but do nothing\n"
	"  -L, --no-locking          Don't try to lock files\n"
	"  -h, --help                Display this information and exit\n"
	"  -V, --version             Show version number and exit\n"
	"\n"
	" File locations used:\n"
	"   master passwd: %s\n"
	"   master group : %s\n"
	"   system passwd: %s\n"
	"   system shadow: %s\n"
	"   system group : %s\n"
	"\n"
	"Report bugs to the Debian bug tracking system, package \"base-passwd\".\n"
	"\n",
	master_passwd, master_group, sys_passwd, sys_shadow, sys_group);
}

/* Simple function to print our name and version
 */
void version(void) {
    printf("update-passwd %s\n", PACKAGE_VERSION);
}


/* Assuming that we've already queued up a debconf question using REGISTER and
 * any necessary SUBST, ask the question and return the answer as a boolean
 * flag.  Aborts the problem on any failure.
 */
int ask_debconf(const char* priority, const char* question) {
#ifdef HAVE_DEBCONF
    int		ret;
    const char*	response;

    ret=debconf_input(debconf, priority, question);
    if (ret==0)
	ret=debconf_go(debconf);
    else if (ret==30)
	ret=0;
    if (ret==0)
	ret=debconf_get(debconf, question);
    if (ret!=0) {
	fprintf(stderr, "Debconf interaction failed\n");
	exit(1);
    }
    response=debconf->ret(debconf);
    if (response!=NULL && strcmp(response, "true")==0)
	return 1;
    else
	return 0;
#else
	return 0;
#endif
}


/* Escape an arbitrary string for use in a debconf question.  We take the
 * conservative approach of replacing all non-alphanumeric characters except
 * hyphen (-) and underscore (_) with underscores.  Returns newly allocated
 * memory that the caller is responsible for freeing.
 */
char* escape_debconf(const char* string) {
    char*	copy;
    char*       p;

    assert(string);

    copy=xstrdup(string);
    for (p=copy; *p!='\0'; p++)
	if (!isalnum((int)*p) && *p!='-' && *p!='_')
	    *p='_';
    return copy;
}


/* Check if we need to move any master file entries above NIS compat
 * switching entries ("+").
 */
void process_moved_entries(const struct _info* lst, struct _node** passwd, struct _node* master, const char* descr) {
    struct _node*	walk=*passwd;

    while (walk) {
	if (strcmp(walk->name, "+")==0) {
	    walk=walk->next;
	    break;
	}
	walk=walk->next;
    }
    while (walk) {
	if (find_by_named_entry(master, walk)) {
	    if (!noautoadd(lst, walk->id)) {
		struct _node*	movednode=walk;
		int		make_change=1;

		if (flag_debconf) {
		    char*	question;
		    char*	template;
		    char*	id;
		    const char*	domain=user_domain;

		    if (strcmp(descr, "group")==0)
			domain=group_domain;
		    question=xasprintf("base-passwd/%s/%s/%s/move", domain, descr, movednode->name);
		    template=xasprintf("base-passwd/%s-move", descr);
		    id=xasprintf("%u", movednode->id);
		    DEBCONF_REGISTER(template, question);
		    DEBCONF_SUBST(question, "name", movednode->name);
		    DEBCONF_SUBST(question, "id", id);
		    make_change=ask_debconf("low", question);
		    free(question);
		    free(template);
		    free(id);
		}

		if (make_change) {
		    if (opt_verbose)
			printf("Moving %s \"%s\" (%u) to before \"+\" entry\n", descr, movednode->name, movednode->id);
		    remove_node(passwd, movednode);
		    add_node(passwd, movednode, 1);
		    flag_dirty++;
		}

		walk=walk->next;
		continue;
	    }
	}
	walk=walk->next;
    }
}


/* Check if new accounts should be made on the system. Please note we don't
 * add accounts to shadow here; those will be made automatically at a later
 * stage where we verify the contents of the shadow database
 */
void process_new_entries(const struct _info* lst, struct _node** passwd, struct _node* master, const char* descr) {
    while (master) {
	if (find_by_named_entry(*passwd, master)==NULL) {
	    struct _node*	newnode;
	    int			make_change=1;

	    if (noautoadd(lst, master->id)) {
		master=master->next;
		continue;
	    }

	    if (flag_debconf) {
		char*		question;
		char*		template;
		char*		id;
		const char*	domain=user_domain;

		if (strcmp(descr, "group")==0)
		    domain=group_domain;
		question=xasprintf("base-passwd/%s/%s/%s/add", domain, descr, master->name);
		template=xasprintf("base-passwd/%s-add", descr);
		id=xasprintf("%u", master->id);
		DEBCONF_REGISTER(template, question);
		DEBCONF_SUBST(question, "name", master->name);
		DEBCONF_SUBST(question, "id", id);
		make_change=ask_debconf("medium", question);
		free(question);
		free(template);
		free(id);
	    }

	    if (make_change) {
		if (opt_verbose)
		    printf("Adding %s \"%s\" (%u)\n", descr, master->name, master->id);
		newnode=copy_node(master);
		add_node(passwd, newnode, 1);
		flag_dirty++;
	    }
	}
	master=master->next;
    }
}


/* Check if accounts should be removed. Like with process_new_accounts we
 * don't update shadow here since it is verified at a later stage anyway.
 * We will only remove accounts in our range (uids 0-99).
 */
void process_old_entries(const struct _info* lst, struct _node** passwd, struct _node* master, const char* descr) {
    struct _node*	walk=*passwd;

    while (walk) {
	if ((walk->id<0) || (walk->id>99)) {
	    walk=walk->next;
	    continue;
	}

	if (noautoremove(lst, walk->id)) {
	    walk=walk->next;
	    continue;
	}

	if (find_by_named_entry(master, walk)==NULL) {
	    struct _node*	oldnode=walk;
	    int			make_change=1;

	    if (flag_debconf) {
		char*		question;
		char*		template;
		char*		id;
		const char*	domain=user_domain;

		if (strcmp(descr, "group")==0)
		    domain=group_domain;
		question=xasprintf("base-passwd/%s/%s/%s/remove", domain, descr, oldnode->name);
		template=xasprintf("base-passwd/%s-remove", descr);
		id=xasprintf("%u", oldnode->id);
		DEBCONF_REGISTER(template, question);
		DEBCONF_SUBST(question, "name", oldnode->name);
		DEBCONF_SUBST(question, "id", id);
		make_change=ask_debconf("high", question);
		free(question);
		free(template);
		free(id);
	    }

	    walk=walk->next;
	    if (make_change) {
		if (opt_verbose)
		    printf("Removing %s \"%s\" (%u)\n", descr, oldnode->name, oldnode->id);
		remove_node(passwd, oldnode);
		flag_dirty++;
	    }
	    continue;
	}
	walk=walk->next;
    }
}


/* Check if account-information needs to be updated.
 */
void process_changed_accounts(struct _node* passwd, struct _node* group, struct _node* master) {
    for (;passwd; passwd=passwd->next) {
	struct _node*	mc;	/* mastercopy of this account */
	char*		question;
	char*		old_id;
	char*		new_id;
	char*		oldpart;
	char*		newpart;
	int		make_change;

	if (((passwd->id<0) || (passwd->id>99)) && (passwd->id!=65534))
	    continue;

	mc=find_by_named_entry(master, passwd);
	if (mc==NULL)
	    continue;

	if (passwd->id!=mc->id) {
	    make_change=1;
	    if (flag_debconf) {
		question=xasprintf("base-passwd/%s/user/%s/uid/%u/%u", user_domain, passwd->name, passwd->id, mc->id);
		old_id=xasprintf("%u", passwd->id);
		new_id=xasprintf("%u", mc->id);
		DEBCONF_REGISTER("base-passwd/user-change-uid", question);
		DEBCONF_SUBST(question, "name", passwd->name);
		DEBCONF_SUBST(question, "old_uid", old_id);
		DEBCONF_SUBST(question, "new_uid", new_id);
		make_change=ask_debconf("high", question);
		free(question);
		free(old_id);
		free(new_id);
	    }

	    if (make_change) {
		if (opt_verbose)
		    printf("Changing uid of %s from %u to %u\n", passwd->name, passwd->id, mc->id);
		passwd->id=mc->id;
		passwd->d.pw.pw_uid=mc->d.pw.pw_uid;
		flag_dirty++;
	    }
	}

	if (passwd->d.pw.pw_gid!=mc->d.pw.pw_gid) {
	    const struct _node* oldentry = find_by_id(group, passwd->d.pw.pw_gid);
	    const struct _node* newentry = find_by_id(group, mc->d.pw.pw_gid);
	    const char* oldname = oldentry ? oldentry->name : "ABSENT";
	    const char* newname = newentry ? newentry->name : "ABSENT";

	    make_change=1;
	    if (flag_debconf) {
		question=xasprintf("base-passwd/%s/user/%s/gid/%u/%u", user_domain, passwd->name, passwd->d.pw.pw_gid, mc->d.pw.pw_gid);
		old_id=xasprintf("%u", passwd->d.pw.pw_gid);
		new_id=xasprintf("%u", mc->d.pw.pw_gid);
		DEBCONF_REGISTER("base-passwd/user-change-gid", question);
		DEBCONF_SUBST(question, "name", passwd->name);
		DEBCONF_SUBST(question, "old_gid", old_id);
		DEBCONF_SUBST(question, "old_group", oldname);
		DEBCONF_SUBST(question, "new_gid", new_id);
		DEBCONF_SUBST(question, "new_group", newname);
		make_change=ask_debconf("high", question);
		free(question);
		free(old_id);
		free(new_id);
	    }

	    if (make_change) {
		if (opt_verbose)
		    printf("Changing gid of %s from %u (%s) to %u (%s)\n", passwd->name, passwd->d.pw.pw_gid, oldname, mc->d.pw.pw_gid, newname);
		passwd->d.pw.pw_gid=mc->d.pw.pw_gid;
		flag_dirty++;
	    }
	}

	if (!keepgecos(specialusers, passwd->id))
	    if ((passwd->d.pw.pw_gecos==NULL) || (strcmp(passwd->d.pw.pw_gecos, mc->d.pw.pw_gecos)!=0)) {
		const char *oldgecos = passwd->d.pw.pw_gecos ? passwd->d.pw.pw_gecos : "";

		make_change=1;
		if (flag_debconf) {
		    question=xasprintf("base-passwd/%s/user/%s/gecos", user_domain, passwd->name);
		    DEBCONF_REGISTER("base-passwd/user-change-gecos", question);
		    DEBCONF_SUBST(question, "name", passwd->name);
		    DEBCONF_SUBST(question, "old_gecos", oldgecos);
		    DEBCONF_SUBST(question, "new_gecos", mc->d.pw.pw_gecos);
		    make_change=ask_debconf("low", question);
		    free(question);
		}

		if (make_change) {
		    if (opt_verbose)
			printf("Changing GECOS of %s from \"%s\" to \"%s\".\n", passwd->name, oldgecos, mc->d.pw.pw_gecos);
		    /* We update the pw_gecos entry of passwd so it now points into the
		     * buffer from mc. This is safe for us, since we know we won't free
		     * the data in mc until after we are done.
		     */
		    passwd->d.pw.pw_gecos=mc->d.pw.pw_gecos;
		    flag_dirty++;
		}
	    }

	if (!keephome(specialusers, passwd->id))
	    if ((passwd->d.pw.pw_dir==NULL) || (strcmp(passwd->d.pw.pw_dir, mc->d.pw.pw_dir)!=0)) {
		const char *olddir = passwd->d.pw.pw_dir ? passwd->d.pw.pw_dir : "(none)";

		make_change=1;
		if (flag_debconf && !((strcmp(passwd->name, "irc")==0) && (strcmp(olddir, "/var/run/ircd")==0))) {
		    oldpart=escape_debconf(olddir);
		    newpart=escape_debconf(mc->d.pw.pw_dir);
		    question=xasprintf("base-passwd/%s/user/%s/home/%s/%s", user_domain, passwd->name, oldpart, newpart);
		    free(oldpart);
		    free(newpart);
		    DEBCONF_REGISTER("base-passwd/user-change-home", question);
		    DEBCONF_SUBST(question, "name", passwd->name);
		    DEBCONF_SUBST(question, "old_home", olddir);
		    DEBCONF_SUBST(question, "new_home", mc->d.pw.pw_dir);
		    make_change=ask_debconf("high", question);
		    free(question);
		}

		if (make_change) {
		    if (opt_verbose)
			printf("Changing home-directory of %s from %s to %s\n", passwd->name, olddir, mc->d.pw.pw_dir);
		    /* We update the pw_dir entry of passwd so it now points into the
		     * buffer from mc. This is safe for us, since we know we won't free
		     * the data in mc until after we are done.
		     */
		    passwd->d.pw.pw_dir=mc->d.pw.pw_dir;
		    flag_dirty++;
		}
	    }

	if (!keepshell(specialusers, passwd->id))
	    if ((passwd->d.pw.pw_shell==NULL) || (strcmp(passwd->d.pw.pw_shell, mc->d.pw.pw_shell)!=0)) {
		const char *oldshell = passwd->d.pw.pw_shell ? passwd->d.pw.pw_shell : "(none)";

		make_change=1;
		if (flag_debconf) {
		    oldpart=escape_debconf(oldshell);
		    newpart=escape_debconf(mc->d.pw.pw_shell);
		    question=xasprintf("base-passwd/%s/user/%s/shell/%s/%s", user_domain, passwd->name, oldpart, newpart);
		    free(oldpart);
		    free(newpart);
		    DEBCONF_REGISTER("base-passwd/user-change-shell", question);
		    DEBCONF_SUBST(question, "name", passwd->name);
		    DEBCONF_SUBST(question, "old_shell", oldshell);
		    DEBCONF_SUBST(question, "new_shell", mc->d.pw.pw_shell);
		    make_change=ask_debconf("medium", question);
		    free(question);
		}

		if (make_change) {
		    if (opt_verbose)
			printf("Changing shell of %s from %s to %s\n", passwd->name, oldshell, mc->d.pw.pw_shell);
		    /* We update the pw_shell entry of passwd so it now points into the
		     * buffer from mc. This is safe for us, since we know we won't free
		     * the data in mc until after we are done.
		     */
		    passwd->d.pw.pw_shell=mc->d.pw.pw_shell;
		    flag_dirty++;
		}
	    }
    }
}


/* Check if account-information needs to be updated.
 */
void process_changed_groups(struct _node* group, struct _node* master) {
    for (;group; group=group->next) {
	struct _node*	mc;	/* mastercopy of this group */

	if (((group->id<0) || (group->id>99)) && (group->id!=65534))
	    continue;

	mc=find_by_named_entry(master, group);
	if (mc==NULL)
	    continue;

	if (group->id!=mc->id) {
	    int	make_change=1;

	    if (flag_debconf) {
		char*	question;
		char*	old_gid;
		char*	new_gid;

		question=xasprintf("base-passwd/%s/group/%s/gid/%u/%u", group_domain, group->name, group->id, mc->id);
		old_gid=xasprintf("%u", group->id);
		new_gid=xasprintf("%u", mc->id);
		DEBCONF_REGISTER("base-passwd/group-change-gid", question);
		DEBCONF_SUBST(question, "name", group->name);
		DEBCONF_SUBST(question, "old_gid", old_gid);
		DEBCONF_SUBST(question, "new_gid", new_gid);
		make_change=ask_debconf("high", question);
		free(question);
		free(old_gid);
		free(new_gid);
	    }

	    if (make_change) {
		if (opt_verbose)
		    printf("Changing gid of %s from %u to %u\n", group->name, group->id, mc->id);
		group->id=mc->id;
		group->d.gr.gr_gid=mc->d.gr.gr_gid;
		flag_dirty++;
	    }
	}
    }
}


int write_passwd(const struct _node* passwd, const char* file) {
    FILE*	output;

    if (opt_verbose>2)
	printf("Writing passwd-file to %s\n", file);

    /* Delete possible left-over file */
    (void) unlink(file);

    if (selinux_prepare_create_file(sys_passwd) != 0) {
        return 0;
    }

    output = fopen(file, "wx");

    selinux_after_create_file();

    if (output == NULL) {
	fprintf(stderr, "Failed to open passwd-file %s for writing: %s\n",
		file, strerror(errno));
	return 0;
    }

    for (;passwd; passwd=passwd->next) {
	assert(passwd->t==t_passwd);
	if (fputpwent(&(passwd->d.pw), output)!=0) {
	    fprintf(stderr, "Error writing passwd-entry: %s\n", strerror(errno));
	    if (fclose(output)!=0) {
		fprintf(stderr, "Error closing passwd-file %s: %s\n", file, strerror(errno));
	    }
	    return 0;
	}
    }

    if (fclose(output)!=0) {
	fprintf(stderr, "Error closing passwd-file %s: %s\n", file, strerror(errno));
	return 0;
    }

    return 1;
}


int write_shadow(const struct _node* shadow, const char* file) {
    FILE*	output;

    if (opt_verbose>2)
	printf("Writing shadow-file to %s\n", file);

    /* Delete possible left-over file */
    (void) unlink(file);

    if (selinux_prepare_create_file(sys_shadow) != 0) {
        return 0;
    }

    output = fopen(file, "wx");

    selinux_after_create_file();

    if (output == NULL) {
	fprintf(stderr, "Failed to open shadow-file %s for writing: %s\n",
	       	file, strerror(errno));
	return 0;
    }

    for (;shadow; shadow=shadow->next) {
	assert(shadow->t==t_shadow);
	if (putspent(&(shadow->d.sp), output)!=0) {
	    fprintf(stderr, "Error writing shadow-entry: %s\n", strerror(errno));
	    if (fclose(output)!=0) {
		fprintf(stderr, "Error closing shadow-file %s: %s\n", file, strerror(errno));
	    }
	    return 0;
	}
    }

    if (fclose(output)!=0) {
	fprintf(stderr, "Error closing shadow-file %s: %s\n", file, strerror(errno));
	return 0;
    }

    return 1;
}


#ifndef HAVE_PUTGRENT
int putgrent(const struct group* g, FILE* f) {
    int idx;
    fprintf(f, "%s:%s:%u:", g->gr_name, g->gr_passwd, g->gr_gid);
    if (g->gr_mem)
	for (idx=0; g->gr_mem[idx]; idx++)
	    fprintf(f, ((idx==0) ? "%s" : ",%s"), g->gr_mem[idx]);
    fprintf(f, "\n");
    return fflush(f);
}
#endif


int write_group(const struct _node* group, const char* file) {
    FILE*	output;

    if (opt_verbose>2)
	printf("Writing group-file to %s\n", file);

    /* Delete possible left-over file */
    (void) unlink(file);


    if (selinux_prepare_create_file(sys_group) != 0) {
        return 0;
    }

    output = fopen(file, "wx");

    selinux_after_create_file();

    if (output == NULL) {
	fprintf(stderr, "Failed to open group-file %s for writing: %s\n",
		file, strerror(errno));
	return 0;
    }

    for (;group; group=group->next) {
	assert(group->t==t_group);
	if (putgrent(&(group->d.gr), output)!=0) {
	    fprintf(stderr, "Error writing group-entry: %s\n", strerror(errno));
	    if (fclose(output)!=0) {
		fprintf(stderr, "Error closing group-file %s: %s\n", file, strerror(errno));
	    }
	    return 0;
	}
    }

    if (fclose(output)!=0) {
	fprintf(stderr, "Error closing group-file %s: %s\n", file, strerror(errno));
	return 0;
    }

    return 1;
}


/* Unlink a file and print an error on failure.
 */
int unlink_file(const char* file) {
    if (unlink(file)!=0) {
	fprintf(stderr, "Error unlinking %s: %s\n", file, strerror(errno));
	return 0;
    }

    return 1;
}


/* Rename a file and print an error on failure.
 */
int rename_file(const char* source, const char* target) {
    if (rename(source, target)!=0) {
	fprintf(stderr, "Error renaming %s to %s: %s\n",
		source, target, strerror(errno));
	return 0;
    }

    return 1;
}


/* Copy the filemodes from one file to another
 */

int copy_filemodes(const char* source, const char* target) {
    struct stat		st;

    if (lstat(source, &st)!=0) {
	fprintf(stderr, "Error lstating %s: %s\n", source, strerror(errno));
	return 0;
    }

    if (chmod(target, st.st_mode)!=0) {
	fprintf(stderr, "Error chmoding %s: %s\n", target, strerror(errno));
	return 0;
    }

    if (lchown(target, st.st_uid, st.st_gid)!=0) {
	/* Hmm, this failed. Lets try a normal chown in case we
	 * are running on a kernel that doesn't support lchown
	 */
	if (errno==ENOSYS) {
	    struct stat           tst;

	    if (lstat (target, &tst)!=0) {
		fprintf (stderr, "Error lstating %s: %s\n",
			target, strerror(errno));
		return 0;
	    }

	    if (!S_ISLNK (tst.st_mode) &&
		    chown (target, st.st_uid, st.st_gid) != 0) {
		fprintf(stderr, "Error lchowning %s: %s\n",
			target, strerror(errno));
		return 0;
	    }
	} else {
	    fprintf(stderr, "Error lchowning %s: %s\n",
		    target, strerror(errno));
	    return 0;
	}
    }

    return 1;
}


/* Atomically replace a file with another, possibly keeping a backup file.
 * Taken from the dpkg2 sourcetree.
 */
int replace_file(const char* org, const char* new, const char* backup) {
    if (backup) {
	int res;

	if ((res=link(org, backup))==-1) {
	    if (errno==EEXIST) {
		if (unlink(backup)==-1) {
		    fprintf(stderr, "Error unlinking old backupfile %s: %s\n",
			    backup, strerror(errno));
		    return 0;
		}
		res=link(org, backup);
	    }
	    if (res==-1) {
		fprintf(stderr, "Error making backupfile %s: %s\n", backup, strerror(errno));
		return 0;
	    }
	}
    }

    if (rename(new, org)==-1) {
	fprintf(stderr, "Error: failed to replace %s with %s: %s\n", org, new, strerror(errno));
	return 0;
    }

    return 1;
}


/* Try to replace a file as safely as possible. If we fail unlink the
 * new copy, since it's useless anyway.
 */
int put_file_in_place(const char* source, const char* target) {
    char*	uf;
    int		ret;

    if (opt_verbose>2)
	printf("Replacing \"%s\" with \"%s\"\n", target, source);

    uf=xasprintf("%s%s", target, BACKUP_EXTENSION);

    if (!copy_filemodes(target, source)) {
	free(uf);
	return 0;
    }

    ret=replace_file(target, source, uf);
    free(uf);

    return ret;
}


/* Rewrite the account-database if we made any changes
 */
int commit_files(void) {
    char*	wf;

    if (!flag_dirty) {
	if (opt_verbose)
	    printf("No changes needed\n");
	return 1;
    }

    if (opt_dryrun) {
	printf("Would commit %d changes\n", flag_dirty);
	return 1;
    }

    printf("%d changes have been made, rewriting files\n", flag_dirty);

    if (opt_verbose==2)
	printf("Writing passwd-file to %s\n", sys_passwd);

    wf=xasprintf("%s%s", sys_passwd, WRITE_EXTENSION);

    if (!write_passwd(system_accounts, wf)) {
	free(wf);
	return 0;
    }

    if (!put_file_in_place(wf, sys_passwd)) {
	free(wf);
	return 0;
    }

    free(wf);

    if (system_shadow!=NULL) {
	if (opt_verbose==2)
	    printf("Writing shadow-file to %s\n", sys_shadow);

	wf=xasprintf("%s%s", sys_shadow, WRITE_EXTENSION);

	if (!write_shadow(system_shadow, wf)) {
	    free(wf);
	    return 0;
	}

	if (!put_file_in_place(wf, sys_shadow)) {
	    free(wf);
	    return 0;
	}

	free(wf);
    }

    if (opt_verbose==2)
	printf("Writing group-file to %s\n", sys_group);

    wf=xasprintf("%s%s", sys_group, WRITE_EXTENSION);

    if (!write_group(system_groups, wf)) {
	free(wf);
	return 0;
    }

    if (!put_file_in_place(wf, sys_group)) {
	free(wf);
	return 0;
    }

    free(wf);

    return 1;
}


/* Try to lock the account database
 */
int lock_files(void) {
    if (lckpwdf()!=0) {
	fprintf(stderr, "Error locking files: %s\n", strerror(errno));
	return 0;
    }

    return 1;
}


/* Try to unlock the account database
 */
int unlock_files(void) {
    if (ulckpwdf()!=0) {
	fprintf(stderr, "Error unlocking files: %s\n", strerror(errno));
	return 0;
    }

    return 1;
}


/* I don't need to say what main is for, do I?
 */
int main(int argc, char** argv) {
    int		optc;
    int		opt_index;

    struct option const options[] = {
	{ "passwd-master",	required_argument,	0,	'p' },
	{ "group-master",	required_argument,	0,	'g' },
	{ "passwd",		required_argument,	0,	'P' },
	{ "shadow",		required_argument,	0,	'S' },
	{ "group",		required_argument,	0,	'G' },
	{ "sanity-check",	no_argument,		0,	's' },
	{ "verbose",		no_argument,		0,	'v' },
	{ "dry-run",		no_argument,		0,	'n' },
	{ "help",		no_argument,		0,	'h' },
	{ "version",		no_argument,		0,	'V' },
	{ 0, 0, 0, 0 }
    };
    
    while ((optc=getopt_long(argc, argv, "g:p:G:P:S:snvLhV", options, &opt_index))!=-1)
	switch (optc)  {
	    case 'p':
		master_passwd=optarg;
		break;
	    case 'g':
		master_group=optarg;
		break;
	    case 'P':
		sys_passwd=optarg;
		user_domain="other";
		break;
	    case 'S':
		sys_shadow=optarg;
		break;
	    case 'G':
		sys_group=optarg;
		group_domain="other";
		break;
	    case 'v':
		opt_verbose++;
		if (opt_verbose==1)
		    opt_verbose++;
		break;
	    case 's':
		opt_sanity=1;
		break;
	    case 'n':
		opt_dryrun=1;
		opt_verbose++;
		break;
	    case 'L':
		opt_nolock=1;
		break;
	    case 'h':
		usage();
		return 0;
	    case 'V':
		version();
		return 0;
	    default:
		fprintf(stderr, "Internal error: getopt_long returned unexpected value \'%c\'\n", optc);
		return 1;
	}

    /* If DEBIAN_HAS_FRONTEND is set in the environment, we're running under
     * debconf.  Enable debconf prompting unless --dry-run was also given.
     */
#ifdef HAVE_DEBCONF
    if (getenv("DEBIAN_HAS_FRONTEND")!=NULL && !opt_dryrun) {
	debconf=debconfclient_new();
	if (debconf==NULL) {
	    fprintf(stderr, "Cannot initialize debconf\n");
	    exit(1);
	}
	flag_debconf=1;
    }
#endif

    if (read_passwd(&master_accounts, master_passwd)!=0)
	return 2;

    if (read_group(&master_groups, master_group)!=0)
	return 2;

    if (read_passwd(&system_accounts, sys_passwd)!=0)
	return 2;

    /* Only abort on a readerror */
    if ((read_shadow(&system_shadow, sys_shadow)!=0) && (errno!=ENOENT))
	return 2;

    if (read_group(&system_groups, sys_group)!=0)
	return 2;

    process_moved_entries(specialgroups, &system_groups, master_groups, "group");
    process_new_entries(specialgroups, &system_groups, master_groups, "group");
    process_old_entries(specialgroups, &system_groups, master_groups, "group");
    process_changed_groups(system_groups, master_groups);

    process_moved_entries(specialusers, &system_accounts, master_accounts, "user");
    process_new_entries(specialusers, &system_accounts, master_accounts, "user");
    process_old_entries(specialusers, &system_accounts, master_accounts, "user");
    process_changed_accounts(system_accounts, system_groups, master_accounts);

    if (opt_sanity)
	return 0;

    if (selinux_setup() != 0)
        return 3;

    if (!opt_nolock && !opt_dryrun)
	if (!lock_files())
	    return 4;

    umask(0077);

    if (!commit_files()) {
	unlock_files();
	return 5;
    }

    if (!opt_nolock && !opt_dryrun)
	if (!unlock_files())
	    return 6;

#ifdef HAVE_DEBCONF
    if (debconf!=NULL)
	debconfclient_delete(debconf);
#endif

    if (opt_dryrun)
	return flag_dirty;
    else
	return 0;
}

/* vim: ts=8 sw=4 cindent si
 */
