Commit Diff


commit - /dev/null
commit + 09a12e37c36bb108ccb3e4556c96ea992d7695b3
blob - /dev/null
blob + fb3216d6cf4e60bc3738ca310dfae4d291ad477a (mode 644)
--- /dev/null
+++ Makefile
@@ -0,0 +1,14 @@
+#	$OpenBSD: Makefile,v 1.1 2015/07/16 20:44:21 tedu Exp $
+
+SRCS=	parse.y vias.c
+
+PROG=	vias
+MAN=	vias.1 vias.conf.5
+
+BINDIR=	/usr/bin
+BINOWN= root
+BINMODE=4555
+
+COPTS+=	-Wall
+
+.include <bsd.prog.mk>
blob - /dev/null
blob + c0e7dc2f71581125e6a029c74e322e4322a91a0b (mode 644)
--- /dev/null
+++ parse.y
@@ -0,0 +1,330 @@
+/* $OpenBSD: parse.y,v 1.19 2016/06/27 15:41:17 tedu Exp $ */
+/*
+ * Copyright (c) 2016 Martijn van Duren <martijn@openbsd.org>
+ * Copyright (c) 2015 Ted Unangst <tedu@openbsd.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.
+ */
+
+%{
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <libgen.h>
+#include <unistd.h>
+#include <stdint.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+#include <err.h>
+
+#include "vias.h"
+
+typedef struct {
+	union {
+		struct {
+			int action;
+			int options;
+			const char **files;
+		};
+		const char *str;
+	};
+	int lineno;
+	int colno;
+} yystype;
+#define YYSTYPE yystype
+
+FILE *yyfp;
+
+struct rule **rules;
+int nrules, maxrules;
+int parse_errors = 0;
+int obsolete_warned = 0;
+
+void yyerror(const char *, ...);
+int yylex(void);
+int yyparse(void);
+
+%}
+
+%token TPERMIT TDENY TOWNER TEDIT
+%token TNOPASS TPERSIST
+%token TSTRING
+
+%%
+
+grammar:	/* empty */
+		| grammar '\n'
+		| grammar rule '\n'
+		| error '\n'
+		;
+
+rule:		action ident target edit {
+			struct rule *r;
+			if ((r = calloc(1, sizeof(*r))) == NULL)
+				err(1, NULL);
+			r->action = $1.action;
+			r->options = $1.options;
+			r->ident = $2.str;
+			r->target = $3.str;
+			r->files = $4.files;
+			if (nrules == maxrules) {
+				if (maxrules == 0)
+					maxrules = 63;
+				else
+					maxrules *= 2;
+				if ((rules = reallocarray(rules, maxrules,
+				    sizeof(*rules))) == NULL)
+					err(1, NULL);
+			}
+			rules[nrules++] = r;
+		} ;
+
+action:		TPERMIT options {
+			$$.action = PERMIT;
+			$$.options = $2.options;
+		} | TDENY {
+			$$.action = DENY;
+			$$.options = 0;
+		} ;
+
+options:	/* none */ {
+			$$.options = 0;
+		} | options option {
+			$$.options = $1.options | $2.options;
+			if (($$.options & (NOPASS|PERSIST)) == (NOPASS|PERSIST)) {
+				yyerror("can't combine nopass and persist");
+				YYERROR;
+			}
+		} ;
+option:		TNOPASS {
+			$$.options = NOPASS;
+		} | TPERSIST {
+			$$.options = PERSIST;
+		};
+
+ident:		TSTRING {
+			$$.str = $1.str;
+		} ;
+
+target:		/* optional */ {
+			$$.str = NULL;
+		} | TAS TSTRING {
+			$$.str = $2.str;
+		} ;
+
+edit:		/* none */ {
+			$$.files = NULL;
+		} | TEDIT files {
+			if (arraylen($2.files))
+				$$.files = $2.files;
+			else {
+				free($2.files);
+				$$.files = NULL;
+			}
+		} ;
+
+files:		/* empty */ {
+			if (($$.files = calloc(1, sizeof(char *))) == NULL)
+				err(1, NULL);
+		} | files TSTRING {
+			int nargs = arraylen($1.files);
+			char *str;
+			int strl, skip = 0;
+			if ($2.str[0] != '/') {
+				warnx("File %s can't be relative", $2.str);
+				skip = 1;
+			}
+			if ((str = realpath($2.str, NULL)) == NULL &&
+			    errno != ENOENT) {
+				warn("Problem verifying %s", $2.str);
+				skip = 1;
+			}
+			if (str != NULL && strcmp($2.str, str)) {
+				strl = strlen(str);
+				if ((str = reallocarray(str, strl + 2,
+				    sizeof(*str))) == NULL)
+					err(1, NULL);
+				str[strl] = '/';
+				str[strl+1] = '\0';
+				if (strcmp($2.str, str)) {
+					warnx("file %s needs to be a resolved "
+					    "path", $2.str);
+					skip = 1;
+				}
+			}
+			free(str);
+			if (!skip) {
+				if (($$.files = reallocarray($1.files, nargs + 2,
+				    sizeof(char *))) == NULL)
+					err(1, NULL);
+				$$.files[nargs] = $2.str;
+				$$.files[nargs + 1] = NULL;
+			}
+		} ;
+
+%%
+
+void
+yyerror(const char *fmt, ...)
+{
+	va_list va;
+
+	fprintf(stderr, "vias: ");
+	va_start(va, fmt);
+	vfprintf(stderr, fmt, va);
+	va_end(va);
+	fprintf(stderr, " at line %d\n", yylval.lineno + 1);
+	parse_errors++;
+}
+
+struct keyword {
+	const char *word;
+	int token;
+} keywords[] = {
+	{ "deny", TDENY },
+	{ "permit", TPERMIT },
+	{ "as", TAS },
+	{ "nopass", TNOPASS },
+	{ "persist", TPERSIST },
+	{ "edit", TEDIT },
+};
+
+int
+yylex(void)
+{
+	char buf[1024], *ebuf, *p, *str;
+	int i, c, quotes = 0, escape = 0, qpos = -1, nonkw = 0;
+
+	p = buf;
+	ebuf = buf + sizeof(buf);
+
+repeat:
+	/* skip whitespace first */
+	for (c = getc(yyfp); c == ' ' || c == '\t'; c = getc(yyfp))
+		yylval.colno++;
+
+	/* check for special one-character constructions */
+	switch (c) {
+		case '\n':
+			yylval.colno = 0;
+			yylval.lineno++;
+			/* FALLTHROUGH */
+		case '{':
+		case '}':
+			return c;
+		case '#':
+			/* skip comments; NUL is allowed; no continuation */
+			while ((c = getc(yyfp)) != '\n')
+				if (c == EOF)
+					goto eof;
+			yylval.colno = 0;
+			yylval.lineno++;
+			return c;
+		case EOF:
+			goto eof;
+	}
+
+	/* parsing next word */
+	for (;; c = getc(yyfp), yylval.colno++) {
+		switch (c) {
+		case '\0':
+			yyerror("unallowed character NUL in column %d",
+			    yylval.colno + 1);
+			escape = 0;
+			continue;
+		case '\\':
+			escape = !escape;
+			if (escape)
+				continue;
+			break;
+		case '\n':
+			if (quotes)
+				yyerror("unterminated quotes in column %d",
+				    qpos + 1);
+			if (escape) {
+				nonkw = 1;
+				escape = 0;
+				yylval.colno = 0;
+				yylval.lineno++;
+				continue;
+			}
+			goto eow;
+		case EOF:
+			if (escape)
+				yyerror("unterminated escape in column %d",
+				    yylval.colno);
+			if (quotes)
+				yyerror("unterminated quotes in column %d",
+				    qpos + 1);
+			goto eow;
+			/* FALLTHROUGH */
+		case '{':
+		case '}':
+		case '#':
+		case ' ':
+		case '\t':
+			if (!escape && !quotes)
+				goto eow;
+			break;
+		case '"':
+			if (!escape) {
+				quotes = !quotes;
+				if (quotes) {
+					nonkw = 1;
+					qpos = yylval.colno;
+				}
+				continue;
+			}
+		}
+		*p++ = c;
+		if (p == ebuf) {
+			yyerror("too long line");
+			p = buf;
+		}
+		escape = 0;
+	}
+
+eow:
+	*p = 0;
+	if (c != EOF)
+		ungetc(c, yyfp);
+	if (p == buf) {
+		/*
+		 * There could be a number of reasons for empty buffer,
+		 * and we handle all of them here, to avoid cluttering
+		 * the main loop.
+		 */
+		if (c == EOF)
+			goto eof;
+		else if (qpos == -1)    /* accept, e.g., empty args: cmd foo args "" */
+			goto repeat;
+	}
+	if (!nonkw) {
+		for (i = 0; i < sizeof(keywords) / sizeof(keywords[0]); i++) {
+			if (strcmp(buf, keywords[i].word) == 0)
+				return keywords[i].token;
+		}
+	}
+	if ((str = strdup(buf)) == NULL)
+		err(1, "strdup");
+	yylval.str = str;
+	return TSTRING;
+
+eof:
+	if (ferror(yyfp))
+		yyerror("input error reading config");
+	return 0;
+}
blob - /dev/null
blob + 1d08973fbf995e8a70855eb6a68969e2408786d7 (mode 644)
--- /dev/null
+++ vias.1
@@ -0,0 +1,102 @@
+.\" $OpenBSD: $
+.\"
+.\"Copyright (c) 2016 Martijn van Duren <martijn@openbsd.org>
+.\"Copyright (c) 2015 Ted Unangst <tedu@openbsd.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: September 2 2016 $
+.Dt VIAS 1
+.Os
+.Sh NAME
+.Nm vias
+.Nd edit a file owned by another user
+.Sh SYNOPSIS
+.Nm vias
+.Op Fl a Ar style
+.Op Fl C Ar config
+.Ar file
+.Op Ar editor flags
+.Sh DESCRIPTION
+The
+.Nm
+utility makes a temporary copy of
+.Ar file
+normaly not accessible by the user and allows the user to edit the temporary
+.Ar file
+with their own editor.
+Upon exit the temporary file is copied back to the original file.
+.Pp
+The options are as follows:
+.Bl -tag -width tenletters
+.It Fl a Ar style
+Use the specified authentication style when validating the user,
+as allowed by
+.Pa /etc/login.conf .
+A list of doas-specific authentication methods may be configured by adding an
+.Sq auth-doas
+entry in
+.Xr login.conf 5 .
+.It Fl C Ar config
+Parse and check the configuration file
+.Ar config ,
+then exit.
+If
+.Ar file 
+is supplied,
+.Nm
+will also try to open the
+.Ar file .
+In the latter case
+either
+.Sq permit ,
+.Sq permit nopass
+or
+.Sq deny
+will be printed on standard output, depending on file
+matching results.
+No copy will be made.
+.El
+.Sh EXIT STATUS
+.Ex -std doas
+It may fail for one of the following reasons:
+.Pp
+.Bl -bullet -compact
+.It
+The config file
+.Pa /etc/vias.conf
+could not be parsed.
+.It
+The user attempted to open a file which is not permitted.
+.It
+The password was incorrect.
+.It
+The 
+.Xr open 2
+command failed.
+.El
+.Sh ENVIRONMENT
+If the following environment variable exists it will be utilized by
+.Nm :
+.Bl -tag -width EDITOR
+.It Ev EDITOR
+The editor specified by the string
+.Ev EDITOR
+will be invoked instead of the default editor
+.Xr vi 1 .
+.El
+
+.Sh SEE ALSO
+.Xr doas 1 ,
+.Xr vias.conf 5
+.Sh AUTHORS
+.An Martijn van Duren Aq Mt martijn@openbsd.org
blob - /dev/null
blob + 244baeac496196d5b096928b125f4e0abbdd42dd (mode 644)
--- /dev/null
+++ vias.c
@@ -0,0 +1,500 @@
+/* $OpenBSD: vias.c,v 1.57 2016/06/19 19:29:43 martijn Exp $ */
+/*
+ * Copyright (c) 2016 Martijn van Duren <martijn@openbsd.org>
+ * Copyright (c) 2015 Ted Unangst <tedu@openbsd.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.
+ */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <sys/ioctl.h>
+
+#include <fcntl.h>
+#include <limits.h>
+#include <libgen.h>
+#include <login_cap.h>
+#include <bsd_auth.h>
+#include <readpassphrase.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <err.h>
+#include <unistd.h>
+#include <pwd.h>
+#include <grp.h>
+#include <syslog.h>
+#include <errno.h>
+
+#include "vias.h"
+
+static void __dead
+usage(void)
+{
+	fprintf(stderr, "usage: vias [-a style] [-C config] "
+	    "file [editor args]\n");
+	exit(1);
+}
+
+size_t
+arraylen(const char **arr)
+{
+	size_t cnt = 0;
+
+	while (*arr) {
+		cnt++;
+		arr++;
+	}
+	return cnt;
+}
+
+static int
+parseuid(const char *s, uid_t *uid)
+{
+	struct passwd *pw;
+	const char *errstr;
+
+	if ((pw = getpwnam(s)) != NULL) {
+		*uid = pw->pw_uid;
+		return 0;
+	}
+	*uid = strtonum(s, 0, UID_MAX, &errstr);
+	if (errstr)
+		return -1;
+	return 0;
+}
+
+static int
+uidcheck(const char *s, uid_t desired)
+{
+	uid_t uid;
+
+	if (parseuid(s, &uid) != 0)
+		return -1;
+	if (uid != desired)
+		return -1;
+	return 0;
+}
+
+static int
+parsegid(const char *s, gid_t *gid)
+{
+	struct group *gr;
+	const char *errstr;
+
+	if ((gr = getgrnam(s)) != NULL) {
+		*gid = gr->gr_gid;
+		return 0;
+	}
+	*gid = strtonum(s, 0, GID_MAX, &errstr);
+	if (errstr)
+		return -1;
+	return 0;
+}
+
+static int
+open_nosym(const char *file)
+{
+	static int dep = 0;
+	struct stat sb;
+	char dir[PATH_MAX];
+	int fd, pfd;
+
+	dep++;
+	(void) strlcpy(dir, dirname(file), sizeof(dir));
+
+	if (dir[1] == '\0') {
+		dep--;
+		if ((pfd = open("/", O_CLOEXEC)) == -1)
+			return -1;
+	} else {
+		pfd = open_nosym(dir);
+		dep--;
+		if (pfd == -1)
+			return -1;
+	}
+
+	do {
+		if (dep) {
+			fd = openat(pfd, basename(file),
+			    O_NOFOLLOW | O_CLOEXEC);
+		} else {
+			fd = openat(pfd, basename(file),
+			    O_RDWR | O_NOFOLLOW | O_CLOEXEC | O_CREAT, 0777);
+		}
+	} while (fd == -1 && errno == EINTR);
+	close(pfd);
+	if (fd == -1)
+		return -1;
+
+	if (fstat(fd, &sb) == -1)
+		err(1, "fstat");
+/*
+ * This should already have been checked in parse.y.
+ * It's only here to test for race-conditions
+ */
+	if (S_ISLNK(sb.st_mode))
+		errx(1, "Symbolic links not allowed");
+	if (dep && !S_ISDIR(sb.st_mode))
+		errc(1, ENOTDIR, NULL);
+	if (!dep && !S_ISREG(sb.st_mode))
+		errx(1, "File is not a regular file");
+
+	return fd;
+}
+
+static int
+match(uid_t uid, gid_t *groups, int ngroups, const char *file, struct rule *r)
+{
+	int i;
+	int flen;
+
+	if (r->ident[0] == ':') {
+		gid_t rgid;
+		if (parsegid(r->ident + 1, &rgid) == -1)
+			return 0;
+		for (i = 0; i < ngroups; i++) {
+			if (rgid == groups[i])
+				break;
+		}
+		if (i == ngroups)
+			return 0;
+	} else {
+		if (uidcheck(r->ident, uid) != 0)
+			return 0;
+	}
+	if (r->files != NULL) {
+		for (i = 0; r->files[i] != NULL; i++) {
+			flen = strlen(r->files[i]);
+/* Allow access to the entire directory tree */
+			if (r->files[i][flen-1] == '/') {
+				if (!strncmp(r->files[i], file, flen))
+					break;
+			} else {
+				if (!strcmp(r->files[i], file))
+					break;
+			}
+		}
+		if (r->files[i] == NULL)
+			return 0;
+	}
+	return 1;
+}
+
+static int
+permit(uid_t uid, gid_t *groups, int ngroups, struct rule **lastr,
+    const char *file)
+{
+	int i;
+	int fd = -1, pfd = -1;
+	uid_t suid = -1;
+	struct rule *r;
+	char *rfile;
+
+	if ((rfile = realpath(file, NULL)) == NULL)
+		err(1, "Unable to open %s", file);
+
+	*lastr = NULL;
+	for (i = 0; i < nrules; i++) {
+		r = rules[i];
+		if (match(uid, groups, ngroups, rfile, r)) {
+/* Try to open the file, so we know the rule is really permitted */
+			if (r->target) {
+				if (parseuid(r->target, &suid) == -1)
+					err(1, "getpwnam");
+				if (seteuid(suid) == -1)
+					err(1, "seteuid");
+			}
+			if ((fd = open_nosym(rfile)) != -1) {
+				if (pfd != -1)
+					if (close(pfd) == -1)
+						err(1, "close");
+				pfd = fd;
+				*lastr = rules[i];
+			} else if (errno != EPERM) {
+				err(1, "open %s", file);
+			}
+			if (r->target && seteuid(0))
+				err(1, "seteuid");
+		}
+	}
+	if (*lastr == NULL || (*lastr)->action != PERMIT) {
+		close(fd);
+		return -1;
+	}
+
+	return fd;
+}
+
+static void
+parseconfig(const char *filename, int checkperms)
+{
+	extern FILE *yyfp;
+	extern int yyparse(void);
+	struct stat sb;
+
+	yyfp = fopen(filename, "r");
+	if (!yyfp)
+		err(1, checkperms ? "vias is not enabled, %s" :
+		    "could not open config file %s", filename);
+
+	if (checkperms) {
+		if (fstat(fileno(yyfp), &sb) != 0)
+			err(1, "fstat(\"%s\")", filename);
+		if ((sb.st_mode & (S_IWGRP|S_IWOTH)) != 0)
+			errx(1, "%s is writable by group or other", filename);
+		if (sb.st_uid != 0)
+			errx(1, "%s is not owned by root", filename);
+	}
+
+	yyparse();
+	fclose(yyfp);
+	if (parse_errors)
+		exit(1);
+}
+
+static void __dead
+checkconfig(const char *confpath, uid_t uid, gid_t *groups, int ngroups,
+    const char *file)
+{
+	struct rule *rule;
+
+	parseconfig(confpath, 0);
+	if (!file)
+		exit(0);
+
+	if (permit(uid, groups, ngroups, &rule, file) != -1) {
+		printf("permit%s\n", (rule->options & NOPASS) ? " nopass" : "");
+		exit(0);
+	}
+	printf("deny\n");
+	exit(1);
+}
+
+static void
+authuser(char *myname, char *login_style, int persist)
+{
+	char *challenge = NULL, *response, rbuf[1024], cbuf[128];
+	auth_session_t *as;
+	int fd = -1;
+
+	if (persist)
+		fd = open("/dev/tty", O_RDWR);
+	if (fd != -1) {
+		if (ioctl(fd, TIOCCHKVERAUTH) == 0)
+			goto good;
+	}
+
+	if (!(as = auth_userchallenge(myname, login_style, "auth-doas",
+	    &challenge)))
+		errx(1, "Authorization failed");
+	if (!challenge) {
+		char host[HOST_NAME_MAX + 1];
+		if (gethostname(host, sizeof(host)))
+			snprintf(host, sizeof(host), "?");
+		snprintf(cbuf, sizeof(cbuf),
+		    "\rvias (%.32s@%.32s) password: ", myname, host);
+		challenge = cbuf;
+	}
+	response = readpassphrase(challenge, rbuf, sizeof(rbuf),
+	    RPP_REQUIRE_TTY);
+	if (response == NULL && errno == ENOTTY) {
+		syslog(LOG_AUTHPRIV | LOG_NOTICE,
+		    "tty required for %s", myname);
+		errx(1, "a tty is required");
+	}
+	if (!auth_userresponse(as, response, 0)) {
+		syslog(LOG_AUTHPRIV | LOG_NOTICE,
+		    "failed auth for %s", myname);
+		errc(1, EPERM, NULL);
+	}
+	explicit_bzero(rbuf, sizeof(rbuf));
+good:
+	if (fd != -1) {
+		int secs = 5 * 60;
+		ioctl(fd, TIOCSETVERAUTH, &secs);
+		close(fd);
+	}
+}
+static int
+fcpy(int dfd, int sfd)
+{
+	unsigned char buf[4096];
+	int r;
+
+	do {
+		while ((r = read(sfd, buf, sizeof(buf))) > 0) {
+			if (write(dfd, buf, r) != r)
+				return 0;
+		}
+	} while (r == -1 && errno == EINTR);
+	if (r == -1)
+		return 0;
+	return 1;
+}
+
+int
+main(int argc, char **argv)
+{
+	const char *confpath = NULL;
+	char tmpfile[] = "/tmp/vias.XXXXXX";
+	char myname[_PW_NAME_LEN + 1];
+	struct passwd *pw;
+	struct rule *rule;
+	uid_t uid;
+	gid_t groups[NGROUPS_MAX + 1];
+	int ngroups;
+	int i, ch;
+	int ofd, tfd;
+	char cwdpath[PATH_MAX];
+	const char *cwd;
+	char *login_style = NULL;
+	char *file;
+	char **eargv;
+	int status;
+	pid_t ret;
+
+	setprogname("vias");
+
+	closefrom(STDERR_FILENO + 1);
+
+	uid = getuid();
+	if (setuid(0) == -1)
+		err(1, "setuid");
+
+	while ((ch = getopt(argc, argv, "a:C:")) != -1) {
+		switch (ch) {
+		case 'a':
+			login_style = optarg;
+			break;
+		case 'C':
+			confpath = optarg;
+			break;
+		default:
+			usage();
+		}
+	}
+	argv += optind;
+	argc -= optind;
+
+	if (argc == 0)
+		usage();
+	file = argv[0];
+	argv++;
+	argc--;
+
+	pw = getpwuid(uid);
+	if (!pw)
+		err(1, "getpwuid failed");
+	if (strlcpy(myname, pw->pw_name, sizeof(myname)) >= sizeof(myname))
+		errx(1, "pw_name too long");
+	ngroups = getgroups(NGROUPS_MAX, groups);
+	if (ngroups == -1)
+		err(1, "can't get groups");
+	groups[ngroups++] = getgid();
+
+	if (confpath)
+		checkconfig(confpath, uid, groups, ngroups, file);
+
+	parseconfig("/etc/vias.conf", 1);
+
+	if ((ofd = permit(uid, groups, ngroups, &rule, file)) == -1) {
+		syslog(LOG_AUTHPRIV | LOG_NOTICE,
+		    "failed edit for %s: %s", myname, file);
+		err(1, "%s", file);
+	}
+
+	if (!(rule->options & NOPASS))
+		authuser(myname, login_style, rule->options & PERSIST);
+
+	if (pledge("stdio rpath wpath cpath exec proc id", NULL) == -1)
+		err(1, "pledge");
+
+	if ((setuid(uid)) == -1)
+		err(1, "setuid failed");
+
+	if (pledge("stdio rpath wpath cpath exec proc", NULL) == -1)
+		err(1, "pledge");
+
+	if ((tfd = mkstemp(tmpfile)) == -1)
+		err(1, "mkstemp failed");
+
+	if (pledge("stdio rpath cpath exec proc", NULL) == -1)
+		err(1, "pledge");
+
+	if (!fcpy(tfd, ofd)) {
+		unlink(tmpfile);
+		err(1, "temp copy failed");
+	}
+
+	if (getcwd(cwdpath, sizeof(cwdpath)) == NULL)
+		cwd = "(failed)";
+	else
+		cwd = cwdpath;
+
+	if (pledge("stdio cpath exec proc", NULL) == -1)
+		err(1, "pledge");
+
+	syslog(LOG_AUTHPRIV | LOG_INFO, "%s edited %s as %s from %s",
+	    myname, file, pw->pw_name, cwd);
+
+	if ((eargv = reallocarray(NULL, arraylen((const char **) argv) + 2,
+	    sizeof(*eargv))) == NULL) {
+		unlink(tmpfile);
+		err(1, NULL);
+	}
+	eargv[0] = getenv("EDITOR");
+	if (eargv[0] == NULL || *(eargv[0]) == '\0')
+		eargv[0] = "vi";
+
+	switch (fork()) {
+	case -1:
+		unlink(tmpfile);
+		err(1, "fork failed");
+	case 0:
+		if (pledge("stdio exec", NULL) == -1)
+			err(1, "pledge");
+
+		for (i = 0; argv[i] != NULL; i++)
+			eargv[i+1] = argv[i];
+		eargv[i+1] = tmpfile;
+		eargv[i+2] = NULL;
+		execvp(eargv[0], eargv);
+		err(1, "execvp failed");
+	default:
+		if (pledge("stdio cpath", NULL) == -1)
+			err(1, "pledge");
+
+		while ((ret = wait(&status)) == -1 && errno == EINTR)
+			;
+		if (ret == -1)
+			err(1, "wait failed: Temporary file saved at %s",
+			    tmpfile);
+	}
+
+	if (WEXITSTATUS(status) != 0) {
+		errx(1, "%s exited with status %d: Temporary file saved at %s",
+		    eargv[0], WEXITSTATUS(status), tmpfile);
+	}
+
+	(void) lseek(tfd, 0, SEEK_SET);
+	if (ftruncate(ofd, 0) == -1)
+		err(1, "ftruncate failed: Temporary file saved at %s", tmpfile);
+	(void) lseek(ofd, 0, SEEK_SET);
+	if (!fcpy(ofd, tfd))
+		err(1, "restoring failed: Temporary file saved at %s", tmpfile);
+	if (unlink(tmpfile) == -1)
+		err(1, "unlink %s", tmpfile);
+	return 0;
+}
blob - /dev/null
blob + 4493c3af54ed59239f89c3f4c86bc221fd38c3b7 (mode 644)
--- /dev/null
+++ vias.conf.5
@@ -0,0 +1,98 @@
+.\" $OpenBSD: $
+.\"
+.\"Copyright (c) 2016 Martijn van Duren <martijn@openbsd.org>
+.\"Copyright (c) 2015 Ted Unangst <tedu@openbsd.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: September 2 2016 $
+.Dt VIAS.CONF 5
+.Os
+.Sh NAME
+.Nm vias.conf
+.Nd vias configuration file
+.Sh SYNOPSIS
+.Nm /etc/vias.conf
+.Sh DESCRIPTION
+The
+.Xr vias 1
+utility allows a user to edit any file as their own user according to the rules
+in the
+.Nm
+configuration file.
+.Pp
+The rules have the following format:
+.Bd -ragged -offset indent
+.Ic permit Ns | Ns Ic deny
+.Op Ar options
+.Ar identity
+.Op Ic as Ar target
+.Op Ic edit Op ...
+.Ed
+.Pp
+Rules consist of the following parts:
+.Bl -tag -width 11n
+.It Ic permit Ns | Ns Ic deny
+The action to be taken if this rule matches.
+.It Ar options
+Options are:
+.Bl -tag -width keepenv
+.It Ic nopass
+The user is not required to enter a password.
+.El
+.It Ar identity
+The username to match.
+Groups may be specified by prepending a colon
+.Pq Sq \&: .
+Numeric IDs are also accepted.
+.It Ic as Ar target
+The
+.Ar target
+user who opens the file.
+This can be used as an extra restriction on opening certain files.
+The system will try to open the file as that user if all other checks match.
+The default is root.
+.It Ic edit Op ...
+A space separated list of files to be matched.
+A file needs to be the full pathname without symlinks as produced by
+.Xr realpath 3 .
+If the filename ends in a slash it allows access on that entire subtree.
+When using the directory syntax it is advised to set
+.Ar target .
+.El
+.Pp
+The last matching rule determines the action taken.
+If no rule matches, the action is denied.
+.Pp
+Comments can be put anywhere in the file using a hash mark
+.Pq Sq # ,
+and extend to the end of the current line.
+.Pp
+The following quoting rules apply:
+.Bl -dash
+.It
+The text between a pair of double quotes
+.Pq Sq \&"
+is taken as is.
+.It
+The backslash character
+.Pq Sq \e
+escapes the next character, including new line characters, outside comments;
+as a result, comments may not be extended over multiple lines.
+.It
+If quotes or backslashes are used in a word,
+it is not considered a keyword.
+.El
+.Sh SEE ALSO
+.Xr vias 1
+.Sh AUTHORS
+.An Martijn van Duren Aq Mt martijn@openbsd.org
blob - /dev/null
blob + 48d88dd7713cf28247a91d7c72781ed611365a32 (mode 644)
--- /dev/null
+++ vias.h
@@ -0,0 +1,20 @@
+/* $OpenBSD: doas.h,v 1.8 2016/06/19 19:29:43 martijn Exp $ */
+struct rule {
+	int action;
+	int options;
+	const char *ident;
+	const char *target;
+	const char **files;
+};
+
+extern struct rule **rules;
+extern int nrules, maxrules;
+extern int parse_errors;
+
+size_t arraylen(const char **);
+
+#define PERMIT	1
+#define DENY	2
+
+#define NOPASS		0x1
+#define PERSIST		0x4