]> git.armaanb.net Git - opendoas.git/commitdiff
import doas. still subject to changes, large and small.
authorTed Unangst <tedu@openbsd.org>
Thu, 16 Jul 2015 20:44:21 +0000 (20:44 +0000)
committerTed Unangst <tedu@openbsd.org>
Thu, 16 Jul 2015 20:44:21 +0000 (20:44 +0000)
Makefile [new file with mode: 0644]
doas.1 [new file with mode: 0644]
doas.c [new file with mode: 0644]
doas.conf.5 [new file with mode: 0644]
doas.h [new file with mode: 0644]
parse.y [new file with mode: 0644]

diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..191d00f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,14 @@
+#      $OpenBSD: Makefile,v 1.9 2014/01/13 01:41:00 tedu Exp $
+
+SRCS=  parse.y doas.c
+
+PROG=  doas
+MAN=   doas.1 doas.conf.5
+
+BINOWN= root
+BINMODE=4555
+
+CFLAGS+= -I${.CURDIR}
+COPTS+=        -Wall
+
+.include <bsd.prog.mk>
diff --git a/doas.1 b/doas.1
new file mode 100644 (file)
index 0000000..d2f94ef
--- /dev/null
+++ b/doas.1
@@ -0,0 +1,57 @@
+.\" $OpenBSD$
+.\"
+.\"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$
+.Dt DOAS 1
+.Os
+.Sh NAME
+.Nm doas
+.Nd execute commands as another user
+.Sh SYNOPSIS
+.Nm doas
+.Op Fl u Ar user
+command
+.Op Ar args
+.Sh DESCRIPTION
+The
+.Nm
+utility executes the given command as another user.
+.Pp
+The options are as follows:
+.Bl -tag -width tenletters
+.It Fl u Ar user
+Execute the command as
+.Ar user .
+The default is root.
+.El
+.Sh EXIT STATUS
+.Ex -std doas
+It may fail because of one of the following reasons:
+.Pp
+.Bl -bullet -compact
+.It
+The config file could not be parsed.
+.It
+The user attempted an command which is not permitted.
+.It
+Entered passphrase is incorrect.
+.El
+.Sh HISTORY
+The
+.Nm
+command first appeared in
+.Ox 5.8 .
+.Sh AUTHORS
+.An Ted Unangst Aq Mt tedu@openbsd.org
diff --git a/doas.c b/doas.c
new file mode 100644 (file)
index 0000000..7c800bd
--- /dev/null
+++ b/doas.c
@@ -0,0 +1,321 @@
+/* $OpenBSD$ */
+/*
+ * 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 <limits.h>
+#include <login_cap.h>
+#include <bsd_auth.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 "doas.h"
+
+static void __dead
+usage(void)
+{
+       fprintf(stderr, "usage: doas [-u user] command [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 gid_t
+strtogid(const char *s)
+{
+       struct group *gr;
+       const char *errstr;
+       gid_t gid;
+
+       if ((gr = getgrnam(s)) != NULL)
+               return gr->gr_gid;
+       gid = strtonum(s, 0, GID_MAX, &errstr);
+       if (errstr)
+               return -1;
+       return gid;
+}
+
+static int
+match(uid_t uid, gid_t *groups, int ngroups, uid_t target, const char *cmd,
+    struct rule *r)
+{
+       int i;
+
+       if (r->ident[0] == ':') {
+               gid_t rgid = strtogid(r->ident + 1);
+               if (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->target && uidcheck(r->target, target) != 0)
+               return 0;
+       if (r->cmd && strcmp(r->cmd, cmd) != 0)
+               return 0;
+       return 1;
+}
+
+static int
+permit(uid_t uid, gid_t *groups, int ngroups, struct rule **lastr,
+    uid_t target, const char *cmd)
+{
+       int i;
+
+       *lastr = NULL;
+       for (i = 0; i < nrules; i++) {
+               if (match(uid, groups, ngroups, target, cmd, rules[i]))
+                       *lastr = rules[i];
+       }
+       if (!*lastr)
+               return 0;
+       return (*lastr)->action == PERMIT;
+}
+
+static void
+parseconfig(const char *filename)
+{
+       extern FILE *yyfp;
+       extern int yyparse(void);
+
+       yyfp = fopen(filename, "r");
+       if (!yyfp) {
+               fprintf(stderr, "doas is not enabled.\n");
+               exit(1);
+       }
+       yyparse();
+       fclose(yyfp);
+}
+
+static int
+copyenvhelper(const char **oldenvp, const char **safeset, int nsafe, char **envp, int ei)
+{
+       int i;
+       for (i = 0; i < nsafe; i++) {
+               const char **oe = oldenvp;
+               while (*oe) {
+                       size_t len = strlen(safeset[i]);
+                       if (strncmp(*oe, safeset[i], len) == 0 &&
+                           (*oe)[len] == '=') {
+                               if (!(envp[ei++] = strdup(*oe)))
+                                       err(1, "strdup");
+                               break;
+                       }
+                       oe++;
+               }
+       }
+       return ei;
+}
+
+static char **
+copyenv(const char **oldenvp, struct rule *rule)
+{
+       const char *safeset[] = {
+               "DISPLAY", "HOME", "LOGNAME", "MAIL", "SHELL",
+               "PATH", "TERM", "USER", "USERNAME",
+               NULL,
+       };
+       int nsafe;
+       int nextras = 0;
+       char **envp;
+       const char **extra;
+       int ei;
+       int i, j;
+       
+       if ((rule->options & KEEPENV) && !rule->envlist) {
+               j = arraylen(oldenvp);
+               envp = reallocarray(NULL, j + 1, sizeof(char *));
+               for (i = 0; i < j; i++) {
+                       if (!(envp[i] = strdup(oldenvp[i])))
+                               err(1, "strdup");
+               }
+               envp[i] = NULL;
+               return envp;
+       }
+
+       nsafe = arraylen(safeset);
+       if ((extra = rule->envlist)) {
+               nextras = arraylen(extra);
+               for (i = 0; i < nsafe; i++) {
+                       for (j = 0; j < nextras; j++) {
+                               if (strcmp(extra[j], safeset[i]) == 0) {
+                                       extra[j--] = extra[nextras--];
+                                       extra[nextras] = NULL;
+                               }
+                       }
+               }
+       }
+
+       envp = reallocarray(NULL, nsafe + nextras + 1, sizeof(char *));
+       if (!envp)
+               err(1, "can't allocate new environment");
+
+       ei = 0;
+       ei = copyenvhelper(oldenvp, safeset, nsafe, envp, ei);
+       ei = copyenvhelper(oldenvp, rule->envlist, nextras, envp, ei);
+       envp[ei] = NULL;
+
+       return envp;
+}
+
+static void __dead
+fail(void)
+{
+       const char *msgs[] = {
+               "No lollygagging!",
+               "Better luck next time.",
+               "PEBKAC detected.",
+               "That's what happens when you're lazy.",
+               "It is clear that this has not been thought through.",
+               "That's the most ridiculous thing I've heard in the last two or three minutes!",
+               "No sane people allowed here.  Go home.",
+               "I would explain, but I am too drunk.",
+               "You're not allowed to have an opinion.",
+               "Complaint forms are handled in another department.",
+       };
+       const char *m;
+
+       m = msgs[arc4random_uniform(sizeof(msgs) / sizeof(msgs[0]))];
+       fprintf(stderr, m);
+       fprintf(stderr, "\n");
+       exit(1);
+}
+
+int
+main(int argc, char **argv, char **envp)
+{
+       char cmdline[1024];
+       char myname[32];
+       uid_t uid, target = 0;
+       gid_t groups[NGROUPS_MAX + 1];
+       int ngroups;
+       struct passwd *pw;
+       struct rule *rule;
+       const char *cmd;
+       int i, ch;
+       const char *safepath = "/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin";
+
+       parseconfig("/etc/doas.conf");
+
+       while ((ch = getopt(argc, argv, "u:")) != -1) {
+               switch (ch) {
+               case 'u':
+                       if (parseuid(optarg, &target) != 0)
+                               errx(1, "unknown user");
+                       break;
+               default:
+                       usage();
+                       break;
+               }
+       }
+       argv += optind;
+       argc -= optind;
+
+       if (!argc)
+               usage();
+
+       cmd = argv[0];
+       strlcpy(cmdline, argv[0], sizeof(cmdline));
+       for (i = 1; i < argc; i++) {
+               strlcat(cmdline, " ", sizeof(cmdline));
+               strlcat(cmdline, argv[i], sizeof(cmdline));
+       }
+
+       uid = getuid();
+       pw = getpwuid(uid);
+       if (!pw)
+               err(1, "getpwuid failed");
+       strlcpy(myname, pw->pw_name, sizeof(myname));
+       ngroups = getgroups(NGROUPS_MAX, groups);
+       if (ngroups == -1)
+               err(1, "can't get groups");
+       groups[ngroups++] = getgid();
+
+       if (!permit(uid, groups, ngroups, &rule, target, cmd)) {
+               syslog(LOG_AUTHPRIV | LOG_NOTICE, "failed command for %s: %s", myname, cmdline);
+               fail();
+       }
+
+       if (!(rule->options & NOPASS)) {
+               if (!auth_userokay(myname, NULL, NULL, NULL)) {
+                       syslog(LOG_AUTHPRIV | LOG_NOTICE, "failed password for %s", myname);
+                       fail();
+               }
+       }
+       envp = copyenv((const char **)envp, rule);
+
+       pw = getpwuid(target);
+       if (!pw)
+               errx(1, "no passwd entry for target");
+       if (setusercontext(NULL, pw, target, LOGIN_SETGROUP |
+           LOGIN_SETPRIORITY | LOGIN_SETRESOURCES | LOGIN_SETUMASK |
+           LOGIN_SETUSER) != 0)
+               errx(1, "failed to set user context for target");
+
+       syslog(LOG_AUTHPRIV | LOG_INFO, "%s ran command as %s: %s", myname, pw->pw_name, cmdline);
+       setenv("PATH", safepath, 1);
+       execvpe(cmd, argv, envp);
+       err(1, "%s", cmd);
+}
diff --git a/doas.conf.5 b/doas.conf.5
new file mode 100644 (file)
index 0000000..3f25bd1
--- /dev/null
@@ -0,0 +1,70 @@
+.\" $OpenBSD$
+.\"
+.\"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$
+.Dt DOAS.CONF 5
+.Os
+.Sh NAME
+.Nm doas.conf
+.Nd doas configuration file
+.Sh DESCRIPTION
+The
+.Xr doas 1
+utility executes commands as other users according to the rules
+in the
+.Nm
+configuration file.
+.Pp
+The rules have the following format:
+.Bd -literal -offset indent
+permit|deny [options] [identity] [as target] [cmd command]
+.Ed
+.Pp
+Rules consist of the following parts:
+.Bl -tag -width tenletters
+.It permit|deny
+The action to be taken if this rule matches.
+.It options
+Options are:
+.Bl -tag -width tenletters
+.It nopass
+The user is not required to enter a password.
+.It keepenv
+The user's environment is maintained.
+The default is to reset the environment.
+.It keepenv { [variable names] }
+Reset the environment, but keep the specified variables.
+.El
+.It identity
+The username to match.
+Groups may be specified by prepending a colon (:).
+Numeric IDs are also accepted.
+.It as target
+The target user the running user is allowed to run the command as.
+The default is root.
+.It cmd command
+The command the user is allowed or denied to run.
+The default is all commands.
+Be advised that it's best to specify absolute paths.
+.El
+.Pp
+The last matching rule determines the action taken.
+.Sh EXAMPLES
+The following example permits users in group wheel to exeucte commands as root,
+and additionally permits tedu to run procmap as root without a password.
+.Bd -literal -offset indent
+permit :wheel
+permit nopass tedu cmd /usr/sbin/procmap
+.Ed
diff --git a/doas.h b/doas.h
new file mode 100644 (file)
index 0000000..b6d0275
--- /dev/null
+++ b/doas.h
@@ -0,0 +1,20 @@
+
+struct rule {
+       int action;
+       int options;
+       const char *ident;
+       const char *target;
+       const char *cmd;
+       const char **envlist;
+};
+
+extern struct rule **rules;
+extern int nrules, maxrules;
+
+size_t arraylen(const char **);
+
+#define PERMIT 1
+#define DENY   2
+
+#define NOPASS         0x1
+#define KEEPENV                0x2
diff --git a/parse.y b/parse.y
new file mode 100644 (file)
index 0000000..4729b4e
--- /dev/null
+++ b/parse.y
@@ -0,0 +1,210 @@
+/* $OpenBSD$ */
+/*
+ * 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 <ctype.h>
+#include <unistd.h>
+#include <stdint.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+#include <err.h>
+
+#include "doas.h"
+
+typedef struct {
+       union {
+               struct {
+                       int action;
+                       int options;
+                       const char **envlist;
+               };
+               const char *str;
+       };
+} yystype;
+#define YYSTYPE yystype
+
+FILE *yyfp;
+
+struct rule **rules;
+int nrules, maxrules;
+
+%}
+
+%token TPERMIT TDENY TAS TCMD
+%token TNOPASS TKEEPENV
+%token TSTRING
+
+%%
+
+grammar:       /* empty */
+               | grammar '\n'
+               | grammar rule '\n'
+               ;
+
+rule:          action ident target cmd {
+                       struct rule *r;
+                       r = calloc(1, sizeof(*r));
+                       r->action = $1.action;
+                       r->options = $1.options;
+                       r->envlist = $1.envlist;
+                       r->ident = $2.str;
+                       r->target = $3.str;
+                       r->cmd = $4.str;
+                       if (nrules == maxrules) {
+                               if (maxrules == 0)
+                                       maxrules = 63;
+                               else
+                                       maxrules *= 2;
+                               if (!(rules = reallocarray(rules, maxrules, sizeof(*rules))))
+                                       errx(1, "can't allocate rules");
+                       }
+                       rules[nrules++] = r;
+               } ;
+
+action:                TPERMIT options {
+                       $$.action = PERMIT;
+                       $$.options = $2.options;
+                       $$.envlist = $2.envlist;
+               } | TDENY {
+                       $$.action = DENY;
+               } ;
+
+options:       /* none */
+               | options option {
+                       $$.options = $1.options | $2.options;
+                       $$.envlist = $1.envlist;
+                       if ($2.envlist) {
+                               if ($$.envlist)
+                                       errx(1, "can't have two keepenv sections");
+                               else
+                                       $$.envlist = $2.envlist;
+                       }
+               } ;
+option:                TNOPASS {
+                       $$.options = NOPASS;
+               } | TKEEPENV {
+                       $$.options = KEEPENV;
+               } | TKEEPENV '{' envlist '}' {
+                       $$.options = KEEPENV;
+                       $$.envlist = $3.envlist;
+               } ;
+
+envlist:       /* empty */ {
+                       if (!($$.envlist = calloc(1, sizeof(char *))))
+                               errx(1, "can't allocate envlist");
+               } | envlist TSTRING {
+                       int nenv = arraylen($1.envlist);
+                       if (!($$.envlist = reallocarray($1.envlist, nenv + 2, sizeof(char *))))
+                               errx(1, "can't allocate envlist");
+                       $$.envlist[nenv] = $2.str;
+                       $$.envlist[nenv + 1] = NULL;
+               }
+
+
+ident:         TSTRING {
+                       $$.str = $1.str;
+               } ;
+
+target:                /* optional */ {
+                       $$.str = NULL;
+               } | TAS TSTRING {
+                       $$.str = $2.str;
+               } ;
+
+cmd:           /* optional */ {
+                       $$.str = NULL;
+               } | TCMD TSTRING {
+                       $$.str = $2.str;
+               } ;
+
+%%
+
+void
+yyerror(const char *fmt, ...)
+{
+       va_list va;
+
+       va_start(va, fmt);
+       fprintf(stderr, "doas: ");
+       vfprintf(stderr, fmt, va);
+       fprintf(stderr, "\n");
+       va_end(va);
+       exit(1);
+}
+
+struct keyword {
+       const char *word;
+       int token;
+} keywords[] = {
+       { "deny", TDENY },
+       { "permit", TPERMIT },
+       { "as", TAS },
+       { "cmd", TCMD },
+       { "nopass", TNOPASS },
+       { "keepenv", TKEEPENV },
+};
+
+int
+yylex(void)
+{
+       char buf[1024], *ebuf, *p, *str;
+       int i, c;
+
+       p = buf;
+       ebuf = buf + sizeof(buf);
+       while ((c = getc(yyfp)) == ' ' || c == '\t')
+               ; /* skip spaces */
+       switch (c) {
+               case '\n':
+               case '{':
+               case '}':
+                       return c;
+               case '#':
+                       while ((c = getc(yyfp)) != '\n' && c != EOF)
+                               ; /* skip comments */
+                       if (c == EOF)
+                               return 0;
+                       return c;
+               case EOF:
+                       return 0;
+               case ':':
+                       *p++ = c;
+                       c = getc(yyfp);
+                       break;
+               default:
+                       break;
+       }
+       while (isalnum(c)) {
+               *p++ = c;
+               if (p == ebuf)
+                       yyerror("too much stuff");
+               c = getc(yyfp);
+       }
+       *p = 0;
+       if (c != EOF)
+               ungetc(c, yyfp);
+       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;
+}