Hi all

I had a bit time to improve the format speciers of the filters. The
filters now support '%u' for the key and '%h' for the table name. This
speciers are choosen to be compatible with login_ldap(8) of openbsd[0].
The '%s' specifier still works as the key for backwords compability.

I also have written a manpage. I hope it is understandable and the troff
is correct.

Patches are attached.

Philipp

[0] I plan to send my version of aldap to openbsd, so openbsd also
benefit from my improvements. But I haven't had the time to do so.
From 8926b3137ad1dad3dfff81408141740d330927a1 Mon Sep 17 00:00:00 2001
From: Philipp Takacs <phil...@bureaucracy.de>
Date: Wed, 28 Aug 2024 09:03:48 +0200
Subject: [PATCH 1/2] aldap add %u and %h filter format specifier

The filter allows now to use %u and %h for key and table.
These specier were chosen to allow compatibility with login_ldap(8).
The %s specifier is an alias for %u.
---
 aldap.c      | 140 ++++++++++++++++++++++++++++++++++-----------------
 aldap.h      |   7 ++-
 table_ldap.c |   5 +-
 3 files changed, 103 insertions(+), 49 deletions(-)

diff --git a/aldap.c b/aldap.c
index 6ead891..0e17cf1 100644
--- a/aldap.c
+++ b/aldap.c
@@ -43,13 +43,13 @@
 #define ALDAP_VERSION 3
 
 static struct ber_element	*ldap_parse_search_filter(struct ber_element *,
-				    const char *, const char *);
+				    const char *, const struct aldap_filter_ctx *);
 static struct ber_element	*ldap_do_parse_search_filter(
-				    struct ber_element *, const char **, const char *);
+				    struct ber_element *, const char **, const struct aldap_filter_ctx *);
 struct aldap_stringset		*aldap_get_stringset(struct ber_element *);
 char				*utoa(char *);
 static int			 isu8cont(unsigned char);
-char				*parseval(const char *, size_t, const char *);
+static char			*parseval(const char *, size_t, const struct aldap_filter_ctx *);
 int				aldap_create_page_control(struct ber_element *,
 				    int, struct aldap_page_control *);
 int				aldap_send(struct aldap *,
@@ -281,7 +281,7 @@ fail:
 
 int
 aldap_search(struct aldap *ldap, char *basedn, enum scope scope, const char *filter,
-    const char *key, char * const *attrs, int typesonly, int sizelimit, int timelimit,
+    const struct aldap_filter_ctx *ctx, char * const *attrs, int typesonly, int sizelimit, int timelimit,
     struct aldap_page_control *page)
 {
 	struct ber_element *root = NULL, *ber, *c;
@@ -308,7 +308,7 @@ aldap_search(struct aldap *ldap, char *basedn, enum scope scope, const char *fil
 		goto fail;
 	}
 
-	if ((ber = ldap_parse_search_filter(ber, filter, key)) == NULL) {
+	if ((ber = ldap_parse_search_filter(ber, filter, ctx)) == NULL) {
 		ldap->err = ALDAP_ERR_PARSER_ERROR;
 		goto fail;
 	}
@@ -967,7 +967,7 @@ aldap_get_stringset(struct ber_element *elm)
  *	NULL, parse failed
  */
 static struct ber_element *
-ldap_parse_search_filter(struct ber_element *ber, const char *filter, const char *key)
+ldap_parse_search_filter(struct ber_element *ber, const char *filter, const struct aldap_filter_ctx *ctx)
 {
 	struct ber_element *elm;
 	const char *cp;
@@ -979,7 +979,7 @@ ldap_parse_search_filter(struct ber_element *ber, const char *filter, const char
 		return (NULL);
 	}
 
-	if ((elm = ldap_do_parse_search_filter(ber, &cp, key)) == NULL)
+	if ((elm = ldap_do_parse_search_filter(ber, &cp, ctx)) == NULL)
 		return (NULL);
 
 	if (*cp != '\0') {
@@ -1009,7 +1009,7 @@ ldap_parse_search_filter(struct ber_element *ber, const char *filter, const char
  *
  */
 static struct ber_element *
-ldap_do_parse_search_filter(struct ber_element *prev, const char **cpp, const char *key)
+ldap_do_parse_search_filter(struct ber_element *prev, const char **cpp, const struct aldap_filter_ctx *ctx)
 {
 	struct ber_element *elm, *root = NULL;
 	char *parsed_val;
@@ -1042,7 +1042,7 @@ ldap_do_parse_search_filter(struct ber_element *prev, const char **cpp, const ch
 
 		while (*cp == '(') {
 			if ((elm =
-			    ldap_do_parse_search_filter(elm, &cp, key)) == NULL)
+			    ldap_do_parse_search_filter(elm, &cp, ctx)) == NULL)
 				goto bad;
 		}
 
@@ -1056,7 +1056,7 @@ ldap_do_parse_search_filter(struct ber_element *prev, const char **cpp, const ch
 		ober_set_header(root, BER_CLASS_CONTEXT, LDAP_FILT_NOT);
 
 		cp++;				/* now points to sub-filter */
-		if ((elm = ldap_do_parse_search_filter(root, &cp, key)) == NULL)
+		if ((elm = ldap_do_parse_search_filter(root, &cp, ctx)) == NULL)
 			goto bad;
 
 		if (*cp != ')')			/* trailing `)` of filter */
@@ -1147,7 +1147,7 @@ ldap_do_parse_search_filter(struct ber_element *prev, const char **cpp, const ch
 				else
 					type = LDAP_FILT_SUBS_ANY;
 
-				if ((parsed_val = parseval(attr_val, len, key)) ==
+				if ((parsed_val = parseval(attr_val, len, ctx)) ==
 				    NULL)
 					goto callfail;
 				elm = ober_add_nstring(elm, parsed_val,
@@ -1162,7 +1162,7 @@ ldap_do_parse_search_filter(struct ber_element *prev, const char **cpp, const ch
 			break;
 		}
 
-		if ((parsed_val = parseval(attr_val, len, key)) == NULL)
+		if ((parsed_val = parseval(attr_val, len, ctx)) == NULL)
 			goto callfail;
 		elm = ober_add_nstring(elm, parsed_val, strlen(parsed_val));
 		free(parsed_val);
@@ -1430,17 +1430,44 @@ isu8cont(unsigned char c)
 	return (c & (0x80 | 0x40)) == 0x80;
 }
 
+static ssize_t
+attach_param(char **buffer, size_t *size, size_t pos, const char *param)
+{
+	char *newbuffer;
+	size_t len, newsize;
+
+	if (!param) {
+		return 0;
+	}
+
+	len = strlen(param);
+	if (!len) {
+		return 0;
+	}
+
+	newsize = *size + len;
+	newbuffer = realloc(*buffer, newsize);
+	if (!newbuffer) {
+		return -1;
+	}
+	memcpy(newbuffer + pos, param, len);
+	*buffer = newbuffer;
+	*size = newsize;
+	return len;
+}
+
 /*
  * Parse a LDAP value
  * notes:
  *	the argument p should be a NUL-terminated sequence of ASCII bytes
  */
-char *
-parseval(const char *p, size_t len, const char *key)
+static char *
+parseval(const char *p, size_t len, const struct aldap_filter_ctx *ctx)
 {
 	char	 hex[3];
-	char	*buffer, *newbuffer;
-	size_t	 i, j, keylen, size, newsize;
+	char	*buffer;
+	size_t	 i, j, size;
+	ssize_t	 paramlen;
 	size = len + 1;
 
 	if ((buffer = calloc(1, size)) == NULL)
@@ -1452,37 +1479,56 @@ parseval(const char *p, size_t len, const char *key)
 			buffer[i] = (char)strtoumax(hex, NULL, 16);
 			j += 3;
 		} else if (p[j] == '%') {
-		       switch (p[j + 1]) {
-		       case '%':
-			       buffer[i] = '%';
-			       j += 2;
-			       break;
-		       case 's':
-			       if (!key) {
-				       free(buffer);
-				       return NULL;
-			       }
-			       keylen = strlen(key);
-			       if (!keylen) {
-				       j += 2;
-				       break;
-			       }
-			       newsize = size + keylen;
-			       if ((newbuffer = realloc(buffer, newsize)) == NULL) {
-				       free(buffer);
-				       return NULL;
-			       }
-			       buffer = newbuffer;
-			       size = newsize;
-			       memcpy(buffer + i, key, keylen);
-			       i += keylen - 1;
-			       j += 2;
-			       break;
-		       default:
-			       buffer[i] = '%';
-			       j++;
-			       break;
-		       }
+			switch (p[j + 1]) {
+			case '%':
+				buffer[i] = '%';
+				j += 2;
+				break;
+			case 's': /* Backwards compability */
+			case 'u': /* username */
+				if (ctx) {
+					paramlen = attach_param(&buffer, &size, i, ctx->username);
+				} else {
+					paramlen = 0;
+				}
+
+				if (paramlen == -1) {
+					free(buffer);
+					return NULL;
+				}
+
+				/* Importend
+				 * the loop increases i (the position on the output) on each iteration
+				 * but this might insert nothing
+				 * so it needs to decrease i by 1 to be on the last written position
+				 */
+				i += paramlen - 1;
+				j += 2;
+				break;
+			case 'h': /* hostname */
+				if (ctx) {
+					paramlen = attach_param(&buffer, &size, i, ctx->hostname);
+				} else {
+					paramlen = 0;
+				}
+
+				if (paramlen == -1) {
+					free(buffer);
+					return NULL;
+				}
+
+				/* Importend
+				 * the loop increases i (the position on the output) on each iteration
+				 * but this might insert nothing
+				 * so it needs to decrease i by 1 to be on the last written position
+				 */
+				i += paramlen - 1;
+				j += 2;
+				break;
+			default:
+				free(buffer);
+				return NULL;
+			}
 		} else {
 			buffer[i] = p[j];
 			j++;
diff --git a/aldap.h b/aldap.h
index 8f45c60..9f69747 100644
--- a/aldap.h
+++ b/aldap.h
@@ -89,6 +89,11 @@ struct aldap_stringset {
 	struct ber_octetstring	*str;
 };
 
+struct aldap_filter_ctx {
+	char *username;
+	char *hostname;
+};
+
 struct aldap_url {
 	int		 protocol;
 	char		*host;
@@ -221,7 +226,7 @@ int	 		 aldap_req_starttls(struct aldap *);
 
 int	 aldap_bind(struct aldap *, char *, char *);
 int	 aldap_unbind(struct aldap *);
-int	 aldap_search(struct aldap *, char *, enum scope, const char *, const char *, char * const *, int, int, int, struct aldap_page_control *);
+int	 aldap_search(struct aldap *, char *, enum scope, const char *, const struct aldap_filter_ctx *, char * const *, int, int, int, struct aldap_page_control *);
 int	 aldap_get_errno(struct aldap *, const char **);
 
 int	 aldap_get_resultcode(struct aldap_message *);
diff --git a/table_ldap.c b/table_ldap.c
index 7d6b267..7dd488c 100644
--- a/table_ldap.c
+++ b/table_ldap.c
@@ -580,6 +580,7 @@ lookup_query(int type)
 static void
 table_ldap_callback(struct request *req)
 {
+	struct aldap_filter_ctx ctx;
 	char		  ldapid[sizeof(int)*2+1];
 	int		  ret;
 	struct query	 *q = lookup_query(req->s);
@@ -614,7 +615,9 @@ table_ldap_callback(struct request *req)
 		return;
 	}
 
-	ret = aldap_search(aldap, basedn, LDAP_SCOPE_SUBTREE, q->filter, req->key, attrs, false, num, 0, NULL);
+	ctx.username = req->key;
+	ctx.hostname = req->table;
+	ret = aldap_search(aldap, basedn, LDAP_SCOPE_SUBTREE, q->filter, &ctx, attrs, false, num, 0, NULL);
 	if (ret < 0) {
 		table_api_error(req->id, req->o, NULL);
 		ldap_open();
-- 
2.39.2

From 5f40004673dd53a1110f1846020ce5395db99216 Mon Sep 17 00:00:00 2001
From: Philipp Takacs <phil...@bureaucracy.de>
Date: Sun, 1 Sep 2024 11:34:12 +0200
Subject: [PATCH 2/2] add manpage

---
 Makefile.am  |   2 +
 table-ldap.5 | 309 +++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 311 insertions(+)
 create mode 100644 table-ldap.5

diff --git a/Makefile.am b/Makefile.am
index 9132a4f..3edfec6 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -4,6 +4,8 @@ table_ldap_SOURCES =	table_ldap.c aldap.c ber.c dict.c log.c table_api.c util.c
 
 LDADD =			$(LIBOBJS)
 
+dist_man5_MANS =	table-ldap.5
+
 EXTRA_DIST =		aldap.h ber.h compat.h config.h.in \
 			dict.h log.h table_api.h util.h
 
diff --git a/table-ldap.5 b/table-ldap.5
new file mode 100644
index 0000000..b41f4b0
--- /dev/null
+++ b/table-ldap.5
@@ -0,0 +1,309 @@
+.\" 
+.\" Copyright (c) 2013 Eric Faurot <e...@openbsd.org>
+.\" Copyright (c) Philipp Takacs <philipp+open...@bureaucracy.de>
+.\" 
+.\" Permission to use, copy, modify, and distribute this software for any
+.\" purpose with or without fee is hereby granted, provided that the above
+.\" copyright notice and this permission notice appear in all copies.
+.\" 
+.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+.\" 
+.Dd $Mdocdate: May 25 2024 $
+.Dt TABLE_LDAP 5
+.Os
+.Sh NAME
+.Nm table_ldap
+.Nd format description for smtpd LDAP tables
+.Sh DESCRIPTION
+This manual page documents the file format of LDAP tables used by the
+.Xr smtpd 8
+mail daemon.
+.Pp
+The format described here applies to tables as defined in
+.Xr smtpd.conf 5 .
+.Sh LDAP TABLE
+A ldap table allows to access userinfo, aliases, domains, and mailaddresses
+that are stored in a directory and are provided by an LDAP implementation.
+.Pp
+The table is used by
+.Xr smtpd 8
+when user information such as user-id and/or
+home directory is required for a delivery, when a domain lookup may be required,
+and/or when looking for an alias.
+.Pp
+A LDAP table consists of a LDAP tree starting from a base.
+Each table lookup will search for the matching objects.
+
+.Sh LDAP TABLE CONFIG FILE
+.Ss COMMON OPTIONS
+.Pp
+.Bl -tag -width Ds
+.It Xo
+.Ic url:
+.Ar url
+.Xc
+This is the LDAP url which is used to connect to your LDAP server.
+For example:
+.Bd -literal -offset indent
+url: ldap://ldap.example.com
+.Ed
+.Pp
+
+.It Xo
+.Ic username:
+.Ar binddn
+.Xc
+The Distinguished Name which is used to bind to the ldap server.
+For example:
+.Bd -literal -offset indent
+username: cn=smtpd,cn=sysaccounts,cn=etc,dc=example,dc=com
+.Ed
+.Pp
+
+.It Xo
+.Ic password:
+.Ar password
+.Xc
+The password for the binddn.
+For example:
+.Bd -literal -offset indent
+password: OpenSMTPDRules!
+.Ed
+.Pp
+
+.It Xo
+.Ic basedn:
+.Ar basedn
+.Xc
+The basedn used for each LDAP search request.
+For example:
+.Bd -literal -offset indent
+basedn cn=users,cn=accounts,dc=example,dc=com
+.Ed
+.El
+
+.Ss SERVICE OPTIONS
+For each service there are two options:
+A ldap filter and a list of the attributes.
+
+.Pp
+.Bl -tag -width Ds
+.It Xo
+.Ic alias_filter:
+.Ar filter
+.Xc
+This filter is used when the table is used in an alias (or virtual alias) context.
+The filter should match all entries for this particular alias.
+.Pp
+
+.It Xo
+.Ic alias_attributes:
+.Ar recipient_atribute
+.Xc
+The attribute which holds the recipients for an alias.
+When the attribute is present multible times,
+then all attribute values are returned.
+.Pp
+
+.It Xo
+.Ic credentials_filter:
+.Ar filter
+.Xc
+This filter is used to look up credentials for a specific user.
+The filter should match exactly one object.
+When the filter match multible objects only the last object returned by the serer is used.
+.Pp
+
+.It Xo
+.Ic credentials_attributes:
+.Ar username_attribute, passwordhash_attribute
+.Xc
+The attributes for username and password hash.
+The password hash must be encoded like the output of
+.Xr smtpctl 8
+encrypt subcommand.
+When one attribute is missing the object is ignored.
+When a attribute is present multiple times,
+then only the first one is used.
+.Pp
+
+.It Xo
+.Ic domain_filter:
+.Ar filter
+.Xc
+The filter for domains is used to lookup a domain.
+.Xr smtpd 8
+currently only checks if a domain exists in the table.
+.Pp
+
+.It Xo
+.Ic domain_attributes:
+.Ar attribute
+.Xc
+This attribute is currently not used, because
+.Xr smtpd 8
+does only check if the domain exists.
+.Pp
+
+.It Xo
+.Ic userinfo_filter:
+.Ar filter
+.Xc
+This filter is to lookup the userinfo for an userbase.
+The filter should match exactly one object.
+When the filter match multible objects only the last object returned by the server is used.
+When a attribute is present multiple times,
+then only the first one is used.
+.Pp
+
+.It Xo
+.Ic userinfo_attributes:
+.Ar uid_attribute, gid_attribute, home_attribute
+.Xc
+The attributes for uid, gid, and the home directory of the user.
+When one attribute is missing the object is ignored.
+When a attribute is present multiple times,
+then only the first one is used.
+.Pp
+
+.It Xo
+.Ic mailaddr_filter:
+.Ar filter
+.Xc
+This filter is to match mailaddres.
+.Xr smtpd 8
+currently only checks if a domain exists in the table.
+.Xr 
+.Pp
+
+.It Xo
+.Ic mailaddr_attributes:
+.Ar attribute
+.Xc
+This attribute is currently not used, because
+.Xr smtpd 8
+does only check if the mailaddr exists.
+.Pp
+
+.It Xo
+.Ic mailaddrmap_filter:
+.Ar filter
+.Xc
+This filter is to map users to mailaddresses.
+The filter should return all object conaining the mailaddress of the user.
+.Xr 
+.Pp
+
+.It Xo
+.Ic mailaddrmap_attributes:
+.Ar mail_attribute
+.Xc
+The attribute containe the mailaddresses of the user.
+When the attribute is present multiple times,
+then all attribute values are returned.
+.El
+
+.Sh FORMAT SPECIFIERS
+
+The filter options are designed to allow per request 
+Filters allow some specifiers in the attribute values of the filter.
+The following speciers are defined:
+
+.Bl -column %s -offset indent
+.It %u    The key of the request
+.It %h    The name of the table
+.It %%    The literal '%'
+.It %s    Depricated alias for the the key
+.El
+
+When a not defined specier is used the filter will not work.
+
+.Sh EXAMPLES
+.Ss USERS EXAMPLE
+This example show how to use the ldap table for user accounts stored in ldap.
+Mails for local users are deliverd to a lmtp server.
+Most of the example works with a normal ldap schema.
+Only for the smtpPassword a schemaextention is required.
+
+.Ic Pa /etc/mail/ldap_users.conf
+.Bd -literal -offset indent
+url: ldap.example.com
+username: uid=smtpd,cn=services,dc=example,dc=com
+password: OpenSMTPDRules!
+basedn: cn=users,dc=example,dc=com
+
+alias_filter: (&(objectclass=posixaccount)(mail=%u))
+alias_attributes: uid
+
+credentials_filter: (&(objectclass=posixaccount)(uid=%u))
+credentials_attributes: uid, smtpPassword
+
+userinfo_filter: (&(objectclass=posixaccount)(uid=%u))
+userinfo_attributes: uidNumber, gidNumber, homedir
+
+mailaddr_filter: (&(objectclass=posixaccount)(mail=%s))
+mailaddr_attributes: mail
+
+mailaddrmap_filter: (&(objectclass=posixaccount)(uid=%s))
+mailaddrmap_attributes: mail
+.Ed
+
+.Ic Pa /etc/mail/smtpd.conf
+.Bd -literal -offset indent
+table users ldap:/etc/mail/ldap_users.conf
+
+pki mail.example.com cert "/etc/ssl/mail.example.com.crt"
+pki mail.example.com key "/etc/ssl/private/mail.example.com.key"
+
+listen on egress port 25 tls pki mail.example.com
+listen on egress port 587 tls-require pki mail.example.com senders <users> auth <users>
+
+match for rcpt-to <users> action "ldapusers"
+match from auth for any action "relay"
+match from any reject
+
+action "ldapusers" lmtp "lmtp.example.com:1234" userbase <users> virtual <users>
+action "relay" relay
+.Ed
+
+With this example a user can auth against the credentials stored in ldap.
+The user can send mails with the addresses specified in ldap as sender.
+Mails for the addresses specified in ldap are maped to the user and deliverd over lmtp.
+
+.Sh BUGS
+
+Currently only check and lookup requests are implemented.
+Fetch and update should also be supported.
+
+The auth request is not suported so the password must be readable by the binddn and formated for smtpd.
+
+Pagination is currently not supported.
+
+.Sh STANDARDS
+.Rs
+.%A J. Sermersheim
+.%D June 2006
+.%R RFC 4511
+.%T Lightweight Directory Access Protocol (LDAP): The Protocol
+.Re
+
+.Rs
+.%A J. Sermersheim
+.%A M. Smith
+.%D June 2006
+.%R RFC 4515
+.%T Lightweight Directory Access Protocol (LDAP): String Representation of Search Filters
+.Re
+
+.Sh SEE ALSO
+.Xr smtpd.conf 5 ,
+.Xr smtpctl 8 ,
+.Xr smtpd 8 ,
+.Xr encrypt 1 ,
+.Xr crypt 3
-- 
2.39.2

Reply via email to