/*
 *  nss.c
 *
 *  nsswitch idmapping functions.
 *
 *  Copyright (c) 2004 The Regents of the University of Michigan.
 *  All rights reserved.
 *
 *  J. Bruce Fields <bfields@umich.edu>
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions
 *  are met:
 *
 *  1. Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *  3. Neither the name of the University nor the names of its
 *     contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
 *  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 *  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 *  DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 *  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 *  BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/types.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pwd.h>
#include <grp.h>
#include <netdb.h>
#include <err.h>
#include <grp.h>
#include <limits.h>
#include <ctype.h>
#include "nfsidmap.h"
#include "nfsidmap_plugin.h"
#include "nfsidmap_private.h"
#include <syslog.h>

static char *get_default_domain(void)
{
	static char default_domain[NFS4_MAX_DOMAIN_LEN] = "";
	if (default_domain[0] == 0) {
		nfs4_get_default_domain(NULL, default_domain, NFS4_MAX_DOMAIN_LEN);
	}
	return default_domain;
}

/*
 * NSS Translation Methods
 *
 * These are all just wrappers around getpwnam and friends;
 * we tack on the given domain to the results of getpwnam when looking up a uid,
 * and ignore the domain entirely when looking up a name.
 */

static int write_name(char *dest, char *localname, char *domain, size_t len,
		      int doappend)
{
	if (doappend || !strchr(localname,'@')) {
		if (strlen(localname) + 1 + strlen(domain) + 1 > len)
			return -ENOMEM; /* XXX: Is there an -ETOOLONG? */
		strcpy(dest, localname);
		strcat(dest, "@");
		strcat(dest, domain);
	} else {
		if (strlen(localname) + 1 > len)
			return -ENOMEM;
		strcpy(dest, localname);
	}
	return 0;
}

static int nss_uid_to_name(uid_t uid, char *domain, char *name, size_t len)
{
	struct passwd *pw = NULL;
	struct passwd pwbuf;
	char *buf;
	size_t buflen = get_pwnam_buflen();
	int err = -ENOMEM;

	buf = malloc(buflen);
	if (!buf)
		goto out;
	if (domain == NULL)
		domain = get_default_domain();
	err = -getpwuid_r(uid, &pwbuf, buf, buflen, &pw);
	if (pw == NULL)
		err = -ENOENT;
	if (err)
		goto out_buf;
	if (get_nostrip() & IDTYPE_USER)
		err = write_name(name, pw->pw_name, domain, len, 0);
	else
		err = write_name(name, pw->pw_name, domain, len, 1);
out_buf:
	free(buf);
out:
	return err;
}

static int nss_gid_to_name(gid_t gid, char *domain, char *name, size_t len)
{
	struct group *gr = NULL;
	struct group grbuf;
	char *buf;
	size_t buflen = get_grnam_buflen();
	int err;

	if (domain == NULL)
		domain = get_default_domain();

	do {
		err = -ENOMEM;
		buf = malloc(buflen);
		if (!buf)
			goto out;
		err = -getgrgid_r(gid, &grbuf, buf, buflen, &gr);
		if (gr == NULL && !err)
			err = -ENOENT;
		if (err == -ERANGE) {
			buflen *= 2;
			free(buf);
		}
	} while (err == -ERANGE);

	if (err)
		goto out_buf;
	if (get_nostrip() & IDTYPE_GROUP)
		err = write_name(name, gr->gr_name, domain, len, 0);
	else
		err = write_name(name, gr->gr_name, domain, len, 1);
out_buf:
	free(buf);
out:
	return err;
}

/* XXX: actually should return error, so can distinguish between
 * memory allocation failure and failure to match domain */
static char *strip_domain(const char *name, const char *domain)
{
	const char *c;
	char *l = NULL;
	int len;

	if (name == NULL)
		goto out;

	c = strrchr(name, '@');
	if (c == NULL && domain != NULL)
		goto out;
	if (c == NULL && domain == NULL) {
		len = strlen(name) + 1;
	} else {
		if (domain && strcasecmp(c + 1, domain) != 0)
			goto out;
		len = c - name;
	}

	l = malloc(len + 1);
	if (l == NULL)
		goto out;
	memcpy(l, name, len);
	l[len] = '\0';
out:
	return l;
}

struct pwbuf {
	struct passwd pwbuf;
	char buf[1];
};

static struct passwd *nss_getpwnam(const char *name, const char *domain,
				   int *err_p, int dostrip)
{
	struct passwd *pw;
	struct pwbuf *buf;
	size_t buflen = get_pwnam_buflen();
	char *localname;
	int err = ENOMEM;

	if (buflen > UINT_MAX)
		goto err;

	buf = malloc(sizeof(*buf) + buflen);
	if (buf == NULL)
		goto err;

	err = EINVAL;
	if (dostrip) {
		localname = strip_domain(name, domain);
		IDMAP_LOG(4, ("nss_getpwnam: name '%s' domain '%s': "
			  "resulting localname '%s'", name, domain, localname));
		if (localname == NULL) {
			IDMAP_LOG(0, ("nss_getpwnam: name '%s' does not map "
				"into domain '%s'", name,
				domain ? domain : "<not-provided>"));
			goto err_free_buf;
		}

		err = getpwnam_r(localname, &buf->pwbuf, buf->buf, buflen, &pw);
		if (pw == NULL && domain != NULL)
			IDMAP_LOG(1,
				("nss_getpwnam: name '%s' not found in domain '%s'",
				localname, domain));
		free(localname);
	} else {
		err = getpwnam_r(name, &buf->pwbuf, buf->buf, buflen, &pw);
		if (pw == NULL)
			IDMAP_LOG(1,
				("nss_getpwnam: name '%s' not found (domain not stripped)", name));
	}
	if (err == 0 && pw != NULL) {
		*err_p = 0;
		return pw;
	} else if (err == 0 && pw == NULL) {
		err = ENOENT;
	}

err_free_buf:
	free(buf);
err:
	*err_p = -err;
	return NULL;
}

static int nss_name_to_uid(char *name, uid_t *uid)
{
	struct passwd *pw = NULL;
	char *domain;
	int err = -ENOENT;

	domain = get_default_domain();
	if (get_nostrip() & IDTYPE_USER) {
		pw = nss_getpwnam(name, domain, &err, 0);
		if (pw != NULL)
			goto out_uid;
	}
	pw = nss_getpwnam(name, domain, &err, 1);
	if (pw == NULL)
		goto out;
out_uid:
	*uid = pw->pw_uid;
	IDMAP_LOG(4, ("nss_name_to_uid: name '%s' uid %u", name, *uid));
	free(pw);
	err = 0;
out:
	return err;
}

static char *reformat_name(const char *name)
{
	const char *domain;
	const char *c;
	const char *d;
	char *l = NULL;
	int len;
	int dlen = 0;
	int i;

	c = strchr(name, '@');
	if (c == NULL)
		goto out;
	len = c - name;
	domain = ++c;
	d = strchr(domain, '.');
	if (d == NULL)
		goto out;
	dlen = d - domain;
	l = malloc(dlen + 1 + len + 1);
	if (l == NULL)
		goto out;
	for (i = 0; i < dlen; i++)
		l[i] = toupper(domain[i]);
	l[dlen] = '\\';
	memcpy(l + dlen + 1, name, len);
	l[dlen + 1 + len] = '\0';
out:
	return l;
}

static int _nss_name_to_gid(char *name, gid_t *gid, int dostrip)
{
	struct group *gr = NULL;
	struct group grbuf;
	char *buf, *domain;
	size_t buflen = get_grnam_buflen();
	int err = -EINVAL;
	char *localname = NULL;
	char *ref_name = NULL;

	domain = get_default_domain();
	if (dostrip) {
		localname = strip_domain(name, domain);
		IDMAP_LOG(4, ("nss_name_to_gid: name '%s' domain '%s': "
			  "resulting localname '%s'", name, domain, localname));
		if (!localname) {
			IDMAP_LOG(0, ("nss_name_to_gid: name '%s' does not map "
				  "into domain '%s'", name, domain));
			goto out;
		}
	} else if (get_reformat_group()) {
		ref_name = reformat_name(name);
		if (ref_name == NULL) {
			IDMAP_LOG(1, ("nss_name_to_gid: failed to reformat name '%s'",
				  name));
			err = -ENOENT;
			goto out;
		}
	}

	err = -ENOMEM;
	if (buflen > UINT_MAX)
		goto out_name;

	do {
		buf = malloc(buflen);
		if (!buf)
			goto out_name;
		if (dostrip)
			err = -getgrnam_r(localname, &grbuf, buf, buflen, &gr);
		else if (get_reformat_group())
			err = -getgrnam_r(ref_name, &grbuf, buf, buflen, &gr);
		else
			err = -getgrnam_r(name, &grbuf, buf, buflen, &gr);
		if (gr == NULL && !err) {
			if (dostrip)
				IDMAP_LOG(1, ("nss_name_to_gid: name '%s' not found "
					  "in domain '%s'", localname, domain));
			else if (get_reformat_group())
				IDMAP_LOG(1, ("nss_name_to_gid: name '%s' not found "
					  "(reformatted)", ref_name));
			else
				IDMAP_LOG(1, ("nss_name_to_gid: name '%s' not found "
					  "(domain not stripped)", name));
			err = -ENOENT;
		}
		if (err == -ERANGE) {
			buflen *= 2;
			free(buf);
		}
	} while (err == -ERANGE);

	if (err)
		goto out_buf;
	*gid = gr->gr_gid;
	IDMAP_LOG(4, ("nss_name_to_gid: name '%s' gid %u", name, *gid));
out_buf:
	free(buf);
out_name:
	free(localname);
	free(ref_name);
out:
	return err;
}

static int nss_name_to_gid(char *name, gid_t *gid)
{
	int err = 0;

	if (get_nostrip() & IDTYPE_GROUP) {
		err = _nss_name_to_gid(name, gid, 0);
		if (!err)
			goto out;
	}
	err = _nss_name_to_gid(name, gid, 1);
out:
	return err;
}

static int nss_gss_princ_to_ids(char *secname, char *princ,
				uid_t *uid, uid_t *gid,
				extra_mapping_params **UNUSED(ex))
{
	struct passwd *pw;
	int err = 0;
	char *princ_realm;
	struct conf_list *realms;
	struct conf_list_node *r;
	int found = 0;

	if (strcmp(secname, "spkm3") == 0)
		return -ENOENT;

	if (strcmp(secname, "krb5") != 0)
		return -EINVAL;

	/* get princ's realm */
	princ_realm = strstr(princ, "@");
	if (princ_realm == NULL)
		return -EINVAL;
	princ_realm++;

	/* get list of "local-equivalent" realms and
	 * check against the principal's realm */
	realms = get_local_realms();
	TAILQ_FOREACH(r, &realms->fields, link) {
		if (strcmp(r->field, princ_realm) == 0) {
			found = 1;
			break;
		}
	}
	if (!found) {
		IDMAP_LOG(1, ("nss_gss_princ_to_ids: Local-Realm '%s': NOT FOUND", 
			princ_realm));
		return -ENOENT;
	}
	/* XXX: this should call something like getgssauthnam instead? */
	pw = nss_getpwnam(princ, NULL, &err, 1);
	if (pw == NULL) {
		err = -ENOENT;
		goto out;
	}
	*uid = pw->pw_uid;
	*gid = pw->pw_gid;
	free(pw);
out:
	return err;
}

static int nss_gss_princ_to_grouplist(char *secname, char *princ,
			       gid_t *groups, int *ngroups,
			       extra_mapping_params **UNUSED(ex))
{
	struct passwd *pw;
	int ret = -EINVAL;

	if (strcmp(secname, "krb5") != 0)
		goto out;
	/* XXX: not quite right?  Need to know default realm? */
	/* XXX: this should call something like getgssauthnam instead? */
	pw = nss_getpwnam(princ, NULL, &ret, 1);
	if (pw == NULL) {
		ret = -ENOENT;
		goto out;
	}
	if (getgrouplist(pw->pw_name, pw->pw_gid, groups, ngroups) < 0)
		ret = -ERANGE;
	free(pw);
out:
	return ret;
}

static int nss_plugin_init(void)
{
	if (nfsidmap_conf_path)
		conf_init_file(nfsidmap_conf_path);
	return 0;
}

/*
 * Called by dlclose(). See dlopen(3) man page
 */
__attribute__((destructor))
static int nss_plugin_term(void)
{
	free_local_realms();
	conf_cleanup();
	return 0;
}


struct trans_func nss_trans = {
	.name		= "nsswitch",
	.init		= nss_plugin_init,
	.princ_to_ids	= nss_gss_princ_to_ids,
	.name_to_uid	= nss_name_to_uid,
	.name_to_gid	= nss_name_to_gid,
	.uid_to_name	= nss_uid_to_name,
	.gid_to_name	= nss_gid_to_name,
	.gss_princ_to_grouplist = nss_gss_princ_to_grouplist,
};

struct trans_func *libnfsidmap_plugin_init(void)
{
	return (&nss_trans);
}
