Patch-Source: https://github.com/Duncaen/OpenDoas/pull/71 (rebased + one extra commit)
--
From a6aa77d9f4b9ad4556e478d6779dbebd4143a98a Mon Sep 17 00:00:00 2001
From: Ariadne Conill <ariadne@dereferenced.org>
Date: Wed, 4 Aug 2021 04:47:04 -0600
Subject: [PATCH] add --with-confdir feature

This adds support for an /etc/doas.d configuration directory as discussed in #61. It is disabled by default.

diff --git a/GNUmakefile b/GNUmakefile
index 9470202..22be971 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -27,6 +27,7 @@ install: ${PROG} ${PAM_DOAS} ${MAN}
 	[ -n "${PAM_DOAS}" ] && chmod 0644 ${DESTDIR}${PAMDIR}/doas || true
 	cp -f doas.1 ${DESTDIR}${MANDIR}/man1
 	cp -f doas.conf.5 ${DESTDIR}${MANDIR}/man5
+	cp -f doas.d.5 ${DESTDIR}${MANDIR}/man5
 
 uninstall:
 	rm -f ${DESTDIR}${BINDIR}/${PROG}
diff --git a/README.md b/README.md
index 20ef9f2..92acded 100644
--- a/README.md
+++ b/README.md
@@ -43,3 +43,12 @@ similar to sudo.
 
 See the comment block in `timestamp.c` for an in-depth description on how
 timestamps are created and checked to be as safe as possible.
+
+### `--with-doas-confdir`
+
+An optional feature can be enabled which will result in `doas` reading configuration
+snippets from `/etc/doas.d`.  These configuration snippets have the same requirements
+as `/etc/doas.conf` (owned by root, not world-writable).
+
+If this feature is enabled, only the `/etc/doas.d` directory is read, and the historical
+`/etc/doas.conf` file is ignored.
\ No newline at end of file
diff --git a/configure b/configure
index 1c5d989..22a078e 100755
--- a/configure
+++ b/configure
@@ -28,6 +28,7 @@ usage: configure [options]
   --without-shadow       disable shadow support
 
   --with-timestamp       enable timestamp support
+  --with-doas-confdir    enable configuration directory support
 
   --uid-max=NUM          set UID_MAX (default 65535)
   --gid-max=NUM          set GID_MAX (default 65535)
@@ -39,6 +40,7 @@ EOF
 
 # defaults
 WITHOUT_TIMESTAMP=yes
+WITHOUT_CONFDIR=yes
 UID_MAX=65535
 GID_MAX=65535
 
@@ -58,6 +60,8 @@ for x; do
 	--target) TARGET=$var ;;
 	--enable-debug) DEBUG=yes ;;
 	--enable-static) BUILD_STATIC=yes ;;
+	--with-doas-confdir) WITHOUT_CONFDIR= ;;
+	--without-doas-confdir) WITHOUT_CONFDIR=yes ;;
 	--with-pam) WITHOUT_PAM=; WITHOUT_SHADOW=yes ;;
 	--with-shadow) WITHOUT_SHADOW=; WITHOUT_PAM=yes ;;
 	--without-pam) WITHOUT_PAM=yes ;;
@@ -565,4 +569,8 @@ fi
 
 printf '#define DOAS_CONF "%s/doas.conf"\n' "${SYSCONFDIR}" >>$CONFIG_H
 
+if [ -z "$WITHOUT_CONFDIR" ]; then
+	printf '#define DOAS_CONFDIR "%s/doas.d"\n' "${SYSCONFDIR}" >>$CONFIG_H
+fi
+
 printf '\n#endif /* CONFIG_H */\n' >>$CONFIG_H
diff --git a/doas.c b/doas.c
index ac3a42a..d77186b 100644
--- a/doas.c
+++ b/doas.c
@@ -35,6 +35,7 @@
 #include <syslog.h>
 #include <errno.h>
 #include <fcntl.h>
+#include <dirent.h>
 
 #include "openbsd.h"
 #include "doas.h"
@@ -155,6 +156,7 @@ permit(uid_t uid, gid_t *groups, int ngroups, const struct rule **lastr,
 static void
 parseconfig(const char *filename, int checkperms)
 {
+	extern const char *yyfn;
 	extern FILE *yyfp;
 	extern int yyparse(void);
 	struct stat sb;
@@ -164,6 +166,8 @@ parseconfig(const char *filename, int checkperms)
 		err(1, checkperms ? "doas is not enabled, %s" :
 		    "could not open config file %s", filename);
 
+	yyfn = filename;
+
 	if (checkperms) {
 		if (fstat(fileno(yyfp), &sb) != 0)
 			err(1, "fstat(\"%s\")", filename);
@@ -174,11 +178,82 @@ parseconfig(const char *filename, int checkperms)
 	}
 
 	yyparse();
+	yyfn = NULL;
+
 	fclose(yyfp);
 	if (parse_errors)
 		exit(1);
 }
 
+#ifdef DOAS_CONFDIR
+static int
+isconfdir(const char *dirpath)
+{
+	struct stat sb;
+
+	if (lstat(dirpath, &sb) != 0) {
+		if (errno != ENOENT)
+			err(1, "lstat(\"%s\")", dirpath);
+
+		errno = ENOTDIR;
+		return 0;
+	}
+
+	if ((sb.st_mode & (S_IFMT)) == S_IFDIR)
+		return 1;
+
+	errno = ENOTDIR;
+	return 0;
+}
+
+static void
+parseconfdir(const char *dirpath, int checkperms)
+{
+	struct dirent **dirent_table;
+	int i, m, dirent_count;
+	char pathbuf[PATH_MAX];
+
+	if (!isconfdir(dirpath))
+		err(1, checkperms ? "doas is not enabled, %s" :
+		    "could not open config directory %s", dirpath);
+
+	dirent_count = scandir(dirpath, &dirent_table, NULL, alphasort);
+	if (dirent_count < 0)
+		err(1, checkperms ? "doas is not enabled, %s" :
+		    "could not open config directory %s", dirpath);
+
+	for (i = 0, m = 0; i < dirent_count; i++)
+	{
+		struct stat sb;
+		size_t pathlen;
+
+		pathlen = snprintf(pathbuf, sizeof pathbuf, "%s/%s", dirpath, dirent_table[i]->d_name);
+		free(dirent_table[i]);
+
+		/* make sure path ends in .conf */
+		if (pathlen < 6)
+			continue;
+
+		if (strcmp(pathbuf + (pathlen - 5), ".conf"))
+			continue;
+
+		if (stat(pathbuf, &sb) != 0)
+			err(1, "stat(\"%s\")", pathbuf);
+
+		if ((sb.st_mode & (S_IFMT)) != S_IFREG)
+			continue;
+
+		parseconfig(pathbuf, checkperms);
+		m++;
+	}
+
+	free(dirent_table);
+
+	if (!m)
+		errx(1, "doas is not enabled, %s: no matching configuration files found\n", dirpath);
+}
+#endif
+
 static void __dead
 checkconfig(const char *confpath, int argc, char **argv,
     uid_t uid, gid_t *groups, int ngroups, uid_t target)
@@ -188,6 +263,11 @@ checkconfig(const char *confpath, int argc, char **argv,
 	if (setresuid(uid, uid, uid) != 0)
 		err(1, "setresuid");
 
+#ifdef DOAS_CONFDIR
+	if (isconfdir(confpath))
+		parseconfdir(confpath, 0);
+	else
+#endif
 	parseconfig(confpath, 0);
 	if (!argc)
 		exit(0);
@@ -330,6 +410,11 @@ main(int argc, char **argv)
 	if (geteuid())
 		errx(1, "not installed setuid");
 
+#ifdef DOAS_CONFDIR
+	if (isconfdir(DOAS_CONFDIR))
+		parseconfdir(DOAS_CONFDIR, 1);
+	else
+#endif
 	parseconfig(DOAS_CONF, 1);
 
 	/* cmdline is used only for logging, no need to abort on truncate */
diff --git a/doas.conf.5 b/doas.conf.5
index e98bfbe..e90d512 100644
--- a/doas.conf.5
+++ b/doas.conf.5
@@ -143,6 +143,7 @@ permit nopass keepenv setenv { PATH } root as root
 .Ed
 .Sh SEE ALSO
 .Xr doas 1 ,
+.Xr doas.d 5 ,
 .Xr syslogd 8
 .Sh HISTORY
 The
diff --git a/doas.d.5 b/doas.d.5
new file mode 100644
index 0000000..c5eaa72
--- /dev/null
+++ b/doas.d.5
@@ -0,0 +1,50 @@
+.\"Copyright (c) 2021 Ariadne Conill <ariadne@dereferenced.org>
+.\"
+.\"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: October 9 2020 $
+.Dt DOAS.D 5
+.Os
+.Sh NAME
+.Nm doas.d
+.Nd doas configuration directory
+.Sh DESCRIPTION
+The
+.Xr doas 1
+utility executes commands as other users according to the rules
+configured in either the configuration file or, optionally, the
+configuration directory.  The preference to use the configuration
+file or configuration directory is determined at compile time,
+.Xr doas 1
+will only consult one or the other.
+.Pp
+Configuration snippets stored in the configuration directory
+follow the same rules as the classic
+.Xr doas 1
+configuration file, documented in
+.Xr doas.conf 5 .
+They must end with the .conf extension, or they will be ignored.
+.Pp
+These snippets are read in alphabetical order and thus can be
+ordered in the same way as other configuration directories.
+.Sh FILES
+.Bl -tag -width /etc/doas.d -compact
+.It Pa /etc/doas.d
+.Xr doas 1
+configuration directory.
+.Sh SEE ALSO
+.Xr doas 1 ,
+.Xr doas.conf 5
+.Sh HISTORY
+The
+.Nm
+configuration directory first appeared in OpenDoas.
diff --git a/parse.y b/parse.y
index 388c2a5..c6d7ebf 100644
--- a/parse.y
+++ b/parse.y
@@ -49,6 +49,7 @@ typedef struct {
 } yystype;
 #define YYSTYPE yystype
 
+const char *yyfn;
 FILE *yyfp;
 
 struct rule **rules;
@@ -203,7 +204,7 @@ yyerror(const char *fmt, ...)
 	va_start(va, fmt);
 	vfprintf(stderr, fmt, va);
 	va_end(va);
-	fprintf(stderr, " at line %d\n", yylval.lineno + 1);
+	fprintf(stderr, " at %s, line %d\n", yyfn, yylval.lineno + 1);
 	parse_errors++;
 }
 
--
From c871cf723cc4cc1045ffc7de380f5271b4c29acf Mon Sep 17 00:00:00 2001
From: Jakub Jirutka <jakub@jirutka.cz>
Date: Sat, 13 May 2023 22:38:23 +0200
Subject: [PATCH 9/9] read both /etc/doas.conf and /etc/doas.d/*.conf if
 confdir is enabled

The current behaviour of the configuration directory was required by the
upstream, but it doesn't conform to established conventions used by
virtually all programs on Linux that support modular configuration, and
what the users naturally expect. Also, when someone used to vanilla
OpenDoas or doas on BSD comes to Alpine, the way they're used to
configuring it (via /etc/doas.conf) will no work without notice! It
has already caused some problems and confusion.

The current behaviour is: if /etc/doas.d exists, there must be at least one
*.conf file and /etc/doas.conf is *ignored*.

Since it doesn't look like the upstream will ever merge this, better to
fix this patch to work as it should from the beginning...

This new behaviour: /etc/doas.conf must always exist (as in unpatched
OpenDoas) and will be read first; if /etc/doas.d exists and there are any
*.conf files, they will be loaded as well.
---
 README.md |  7 +++----
 doas.c    | 22 +++++-----------------
 doas.d.5  | 14 +++++---------
 3 files changed, 13 insertions(+), 30 deletions(-)

diff --git a/README.md b/README.md
index 92acded..d5b2e72 100644
--- a/README.md
+++ b/README.md
@@ -48,7 +48,6 @@ timestamps are created and checked to be as safe as possible.
 
 An optional feature can be enabled which will result in `doas` reading configuration
 snippets from `/etc/doas.d`.  These configuration snippets have the same requirements
-as `/etc/doas.conf` (owned by root, not world-writable).
-
-If this feature is enabled, only the `/etc/doas.d` directory is read, and the historical
-`/etc/doas.conf` file is ignored.
\ No newline at end of file
+as `/etc/doas.conf` (owned by root, not world-writable).  The main configuration file
+`/etc/doas.conf` is still required to exist and it is read before `/etc/doas.d`.  It is
+not an error if `/etc/doas.d` does not exist or no matching files are found there.
diff --git a/doas.c b/doas.c
index d77186b..affbe39 100644
--- a/doas.c
+++ b/doas.c
@@ -210,19 +210,14 @@ static void
 parseconfdir(const char *dirpath, int checkperms)
 {
 	struct dirent **dirent_table;
-	int i, m, dirent_count;
+	int i, dirent_count;
 	char pathbuf[PATH_MAX];
 
-	if (!isconfdir(dirpath))
-		err(1, checkperms ? "doas is not enabled, %s" :
-		    "could not open config directory %s", dirpath);
-
 	dirent_count = scandir(dirpath, &dirent_table, NULL, alphasort);
 	if (dirent_count < 0)
-		err(1, checkperms ? "doas is not enabled, %s" :
-		    "could not open config directory %s", dirpath);
+		return;
 
-	for (i = 0, m = 0; i < dirent_count; i++)
+	for (i = 0; i < dirent_count; i++)
 	{
 		struct stat sb;
 		size_t pathlen;
@@ -244,13 +239,9 @@ parseconfdir(const char *dirpath, int checkperms)
 			continue;
 
 		parseconfig(pathbuf, checkperms);
-		m++;
 	}
 
 	free(dirent_table);
-
-	if (!m)
-		errx(1, "doas is not enabled, %s: no matching configuration files found\n", dirpath);
 }
 #endif
 
@@ -263,12 +254,11 @@ checkconfig(const char *confpath, int argc, char **argv,
 	if (setresuid(uid, uid, uid) != 0)
 		err(1, "setresuid");
 
+	parseconfig(confpath, 0);
 #ifdef DOAS_CONFDIR
 	if (isconfdir(confpath))
 		parseconfdir(confpath, 0);
-	else
 #endif
-	parseconfig(confpath, 0);
 	if (!argc)
 		exit(0);
 
@@ -410,13 +400,11 @@ main(int argc, char **argv)
 	if (geteuid())
 		errx(1, "not installed setuid");
 
+	parseconfig(DOAS_CONF, 1);
 #ifdef DOAS_CONFDIR
 	if (isconfdir(DOAS_CONFDIR))
 		parseconfdir(DOAS_CONFDIR, 1);
-	else
 #endif
-	parseconfig(DOAS_CONF, 1);
-
 	/* cmdline is used only for logging, no need to abort on truncate */
 	(void)strlcpy(cmdline, argv[0], sizeof(cmdline));
 	for (i = 1; i < argc; i++) {
diff --git a/doas.d.5 b/doas.d.5
index c5eaa72..5911a12 100644
--- a/doas.d.5
+++ b/doas.d.5
@@ -21,14 +21,9 @@
 The
 .Xr doas 1
 utility executes commands as other users according to the rules
-configured in either the configuration file or, optionally, the
-configuration directory.  The preference to use the configuration
-file or configuration directory is determined at compile time,
-.Xr doas 1
-will only consult one or the other.
-.Pp
-Configuration snippets stored in the configuration directory
-follow the same rules as the classic
+configured in the configuration file and, optionally, the
+configuration directory.  Configuration snippets stored in the
+configuration directory follow the same rules as the classic
 .Xr doas 1
 configuration file, documented in
 .Xr doas.conf 5 .
@@ -47,4 +42,5 @@ configuration directory.
 .Sh HISTORY
 The
 .Nm
-configuration directory first appeared in OpenDoas.
+configuration directory first appeared as a patch for doas on
+Alpine Linux and it is not supported in upstream OpenDoas.
