From 4db1bb1c462389848168a4b79723a6d2ae38f422 Mon Sep 17 00:00:00 2001
From: Scott Mayhew <smayhew@redhat.com>
Date: Wed, 21 Dec 2016 14:43:24 -0500
Subject: [PATCH] libnfsidmap: add options to aid id mapping in multi domain
 environments

This commit adds two options for the nsswitch plugin: No-Strip and
Reformat-Group.

In multi-domain environments, some NFS servers will append the
identity management domain to the owner and owner_group in lieu of a
true NFSv4 domain.  If No-Strip is set to a value other than "none",
the nsswitch plugin will first pass the name to the getpwnam_r() /
getgrnam_r() without stripping the domain off.  If that mapping fails
then the plugin will try again using the old method (comparing the
domain in the string to the Domain value, stripping it if it matches,
and passing the resulting short name to getpwnam_r() / getgrnam_r()).

The Reformat-Group option is used to work around a quirk in Winbind
whereby doing a group lookup in UPN format (e.g.
staff@americas.example.com) will cause the group to be displayed
prefixed with the full domain in uppercase (e.g.
AMERICAS.EXAMPLE.COM\staff) instead of in the familiar netbios
name format (e.g. AMERICAS\staff).  Setting this option to true
causes the name to be reformatted before passing it to getgrnam_r().

These options affect the behavior of of the name_to_uid, name_to_gid,
uid_to_name, and gid_to_name functions, so they work with both the
nfsidmap and rpc.idmapd programs.  These options do not change the
behavior of the princ_to_ids or gss_princ_to_grouplist functions.  Both
of those are used by rpc.svcgssd, which is deprecated in favor of
gssproxy (which does not call either of those functions).

Signed-off-by: Scott Mayhew <smayhew@redhat.com>
Signed-off-by: Steve Dickson <steved@redhat.com>
---
 idmapd.conf         |   23 ++++++
 idmapd.conf.5       |   24 +++++++
 libnfsidmap.c       |   24 +++++++
 nfsidmap_internal.h |    2 
 nss.c               |  178 +++++++++++++++++++++++++++++++++++++++++-----------
 5 files changed, 216 insertions(+), 35 deletions(-)

--- a/idmapd.conf
+++ b/idmapd.conf
@@ -4,6 +4,29 @@
 # The default is the host's DNS domain name.
 #Domain = local.domain.edu
 
+# In multi-domain environments, some NFS servers will append the identity
+# management domain to the owner and owner_group in lieu of a true NFSv4
+# domain.  This option can facilitate lookups in such environments.  If
+# set to a value other than "none", the nsswitch  plugin will first pass
+# the name to the password/group lookup function without stripping the
+# domain off.  If that mapping fails then the plugin will try again using
+# the old method (comparing the domain in the string to the Domain value,
+# stripping it if it matches, and passing the resulting short name to the
+# lookup function).  Valid values are "user", "group", "both", and
+# "none".  The default is "none".
+#No-Strip = none
+
+# Winbind has a quirk whereby doing a group lookup in UPN format
+# (e.g. staff@americas.example.com) will cause the group to be
+# displayed prefixed with the full domain in uppercase
+# (e.g. AMERICAS.EXAMPLE.COM\staff) instead of in the familiar netbios
+# name format (e.g. AMERICAS\staff).  Setting this option to true
+# causes the name to be reformatted before passing it to the group
+# lookup function in order to work around this.  This setting is
+# ignored unless No-Strip is set to either "both" or "group".
+# The default is "false".
+#Reformat-Group = false
+
 # The following is a comma-separated list of Kerberos realm
 # names that should be considered to be equivalent to the
 # local realm, such that <user>@REALM.A can be assumed to
--- a/idmapd.conf.5
+++ b/idmapd.conf.5
@@ -63,6 +63,30 @@ The local NFSv4 domain name.  An NFSv4 d
 a unique username<->UID and groupname<->GID mapping.
 (Default: Host's fully-qualified DNS domain name)
 .TP
+.B No-Strip
+In multi-domain environments, some NFS servers will append the identity
+management domain to the owner and owner_group in lieu of a true NFSv4
+domain.  This option can facilitate lookups in such environments.  If
+set to a value other than "none", the nsswitch  plugin will first pass
+the name to the password/group lookup function without stripping the
+domain off.  If that mapping fails then the plugin will try again using
+the old method (comparing the domain in the string to the Domain value,
+stripping it if it matches, and passing the resulting short name to the
+lookup function).  Valid values are "user", "group", "both", and
+"none".
+(Default: "none")
+.TP
+.B Reformat-Group
+Winbind has a quirk whereby doing a group lookup in UPN format
+(e.g. staff@americas.example.com) will cause the group to be
+displayed prefixed with the full domain in uppercase
+(e.g. AMERICAS.EXAMPLE.COM\\staff) instead of in the familiar netbios
+name format (e.g. AMERICAS\\staff).  Setting this option to true
+causes the name to be reformatted before passing it to the group
+lookup function in order to work around this.  This setting is
+ignored unless No-Strip is set to either "both" or "group".
+(Default: "false")
+.TP
 .B Local-Realms
 A comma-separated list of Kerberos realm names that may be considered equivalent to the
 local realm name.  For example, users juser@ORDER.EDU and juser@MAIL.ORDER.EDU
--- a/libnfsidmap.c
+++ b/libnfsidmap.c
@@ -60,6 +60,8 @@
 static char *default_domain;
 static struct conf_list *local_realms;
 int idmap_verbosity = 0;
+int no_strip = 0;
+int reformat_group = 0;
 static struct mapping_plugin **nfs4_plugins = NULL;
 static struct mapping_plugin **gss_plugins = NULL;
 
@@ -224,6 +226,8 @@ int nfs4_init_name_mapping(char *conffil
 	int ret = -ENOENT;
 	int dflt = 0;
 	struct conf_list *nfs4_methods, *gss_methods;
+	char *nostrip;
+	char *reformatgroup;
 
 	/* XXX: need to be able to reload configurations... */
 	if (nfs4_plugins) /* already succesfully initialized */
@@ -295,6 +299,26 @@ int nfs4_init_name_mapping(char *conffil
 			IDMAP_LOG(1, ("libnfsidmap: Realms list: <NULL> "));
 	}
 
+	nostrip = conf_get_str_with_def("General", "No-Strip", "none");
+	if (strcasecmp(nostrip, "both") == 0)
+		no_strip = IDTYPE_USER|IDTYPE_GROUP;
+	else if (strcasecmp(nostrip, "group") == 0)
+		no_strip = IDTYPE_GROUP;
+	else if (strcasecmp(nostrip, "user") == 0)
+		no_strip = IDTYPE_USER;
+	else
+		no_strip = 0;
+
+	if (no_strip & IDTYPE_GROUP) {
+		reformatgroup = conf_get_str_with_def("General", "Reformat-Group", "false");
+		if ((strcasecmp(reformatgroup, "true") == 0) ||
+		    (strcasecmp(reformatgroup, "on") == 0) ||
+		    (strcasecmp(reformatgroup, "yes") == 0))
+			reformat_group = 1;
+		else
+			reformat_group = 0;
+	}
+
 	nfs4_methods = conf_get_list("Translation", "Method");
 	if (nfs4_methods) {
 		IDMAP_LOG(1, ("libnfsidmap: processing 'Method' list"));
--- a/nfsidmap_internal.h
+++ b/nfsidmap_internal.h
@@ -63,6 +63,8 @@ typedef enum {
 	IDTYPE_GROUP = 2
 } idtypes;
 
+extern int no_strip;
+extern int reformat_group;
 extern int idmap_verbosity;
 extern nfs4_idmap_log_function_t idmap_log_func;
 /* Level zero always prints, others print depending on verbosity level */
--- a/nss.c
+++ b/nss.c
@@ -58,14 +58,20 @@
  * and ignore the domain entirely when looking up a name.
  */
 
-static int write_name(char *dest, char *localname, char *domain, size_t len)
+static int write_name(char *dest, char *localname, char *domain, size_t len,
+		      int doappend)
 {
-	if (strlen(localname) + 1 + strlen(domain) + 1 > len) {
-		return -ENOMEM; /* XXX: Is there an -ETOOLONG? */
+	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);
 	}
-	strcpy(dest, localname);
-	strcat(dest, "@");
-	strcat(dest, domain);
 	return 0;
 }
 
@@ -87,7 +93,10 @@ static int nss_uid_to_name(uid_t uid, ch
 		err = -ENOENT;
 	if (err)
 		goto out_buf;
-	err = write_name(name, pw->pw_name, domain, len);
+	if (no_strip & 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:
@@ -121,7 +130,10 @@ static int nss_gid_to_name(gid_t gid, ch
 
 	if (err)
 		goto out_buf;
-	err = write_name(name, gr->gr_name, domain, len);
+	if (no_strip & 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:
@@ -161,7 +173,8 @@ struct pwbuf {
 	char buf[1];
 };
 
-static struct passwd *nss_getpwnam(const char *name, const char *domain, int *err_p)
+static struct passwd *nss_getpwnam(const char *name, const char *domain,
+				   int *err_p, int dostrip)
 {
 	struct passwd *pw;
 	struct pwbuf *buf;
@@ -174,22 +187,29 @@ static struct passwd *nss_getpwnam(const
 		goto err;
 
 	err = EINVAL;
-	localname = strip_domain(name, domain);
-	IDMAP_LOG(4, ("nss_getpwnam: name '%s' domain '%s': "
-		  "resulting localname '%s'\n", name, domain, localname));
-	if (localname == NULL) {
-		IDMAP_LOG(0, ("nss_getpwnam: name '%s' does not map "
-			"into domain '%s'\n", name,
-			domain ? domain : "<not-provided>"));
-		goto err_free_buf;
-	}
+	if (dostrip) {
+		localname = strip_domain(name, domain);
+		IDMAP_LOG(4, ("nss_getpwnam: name '%s' domain '%s': "
+			  "resulting localname '%s'\n", name, domain, localname));
+		if (localname == NULL) {
+			IDMAP_LOG(0, ("nss_getpwnam: name '%s' does not map "
+				"into domain '%s'\n", 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(0,
-			("nss_getpwnam: name '%s' not found in domain '%s'\n",
-			localname, domain));
-	free(localname);
+		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'\n",
+				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;
@@ -211,37 +231,107 @@ static int nss_name_to_uid(char *name, u
 	int err = -ENOENT;
 
 	domain = get_default_domain();
-	pw = nss_getpwnam(name, domain, &err);
+	if (no_strip & 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 int nss_name_to_gid(char *name, gid_t *gid)
+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, *localname, *domain;
+	char *buf, *domain;
 	size_t buflen = sysconf(_SC_GETGR_R_SIZE_MAX);
 	int err = -EINVAL;
+	char *localname = NULL;
+	char *ref_name = NULL;
 
 	domain = get_default_domain();
-	localname = strip_domain(name, domain);
-	if (!localname)
-		goto out;
+	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 (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;
+		}
+	}
 
 	do {
 		err = -ENOMEM;
 		buf = malloc(buflen);
 		if (!buf)
 			goto out_name;
-		err = -getgrnam_r(localname, &grbuf, buf, buflen, &gr);
-		if (gr == NULL && !err)
+		if (dostrip)
+			err = -getgrnam_r(localname, &grbuf, buf, buflen, &gr);
+		else if (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 (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);
@@ -251,10 +341,28 @@ static int nss_name_to_gid(char *name, g
 	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);
+	if (dostrip)
+		free(localname);
+	if (reformat_group)
+		free(ref_name);
+out:
+	return err;
+}
+
+static int nss_name_to_gid(char *name, gid_t *gid)
+{
+	int err = 0;
+
+	if (no_strip & 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;
 }
@@ -297,7 +405,7 @@ static int nss_gss_princ_to_ids(char *se
 		return -ENOENT;
 	}
 	/* XXX: this should call something like getgssauthnam instead? */
-	pw = nss_getpwnam(princ, NULL, &err);
+	pw = nss_getpwnam(princ, NULL, &err, 0);
 	if (pw == NULL) {
 		err = -ENOENT;
 		goto out;
@@ -320,7 +428,7 @@ int nss_gss_princ_to_grouplist(char *sec
 		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);
+	pw = nss_getpwnam(princ, NULL, &ret, 0);
 	if (pw == NULL) {
 		ret = -ENOENT;
 		goto out;
