"GOT", but the "O" is a cute, smiling sun Index | Thread

From:
Stefan Sperling <stsp@stsp.name>
Subject:
gotd auth rules
To:
gameoftrees@openbsd.org
Date:
Tue, 15 Nov 2022 16:59:18 +0100

Download raw body.

This implements mandatory per-repository read/write authorization
rules in gotd. Mandatory means that repository access will be denied
to anyone if no access rules are configured.

This is still done in the main process. I want to get the config syntax
and auth logic settled before starting a refactoring for privsep.

ok?
 
diff 86b188ee113cde1b53e1d3544b40ce80ab7767a7 7b54830c614ad3ec64f663342de6c9e4183a42a4
commit - 86b188ee113cde1b53e1d3544b40ce80ab7767a7
commit + 7b54830c614ad3ec64f663342de6c9e4183a42a4
blob - 0d22c3ab2c4d40606c11c44bd82449f367e2a9bf
blob + c10b6a5df87a4c719d3bb926aff2316341052ac3
--- gotd/Makefile
+++ gotd/Makefile
@@ -7,8 +7,8 @@ SRCS=		gotd.c repo_read.c repo_write.c log.c privsep_s
 .endif
 
 PROG=		gotd
-SRCS=		gotd.c repo_read.c repo_write.c log.c privsep_stub.c imsg.c \
-		parse.y pack_create.c ratelimit.c deltify.c \
+SRCS=		gotd.c auth.c repo_read.c repo_write.c log.c privsep_stub.c \
+		imsg.c parse.y pack_create.c ratelimit.c deltify.c \
 		bloom.c buf.c date.c deflate.c delta.c delta_cache.c error.c \
 		gitconfig.c gotconfig.c inflate.c lockfile.c murmurhash2.c \
 		object.c object_cache.c object_create.c object_idset.c \
blob - /dev/null
blob + b8b141d19d580a01c1cc68f758b798a5f2947b11 (mode 644)
--- /dev/null
+++ gotd/auth.c
@@ -0,0 +1,135 @@
+/*
+ * Copyright (c) 2022 Stefan Sperling <stsp@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/queue.h>
+#include <sys/uio.h>
+
+#include <errno.h>
+#include <event.h>
+#include <limits.h>
+#include <pwd.h>
+#include <grp.h>
+#include <sha1.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <imsg.h>
+
+#include "got_error.h"
+
+#include "gotd.h"
+#include "auth.h"
+
+static int
+parseuid(const char *s, uid_t *uid)
+{
+	struct passwd *pw;
+	const char *errstr;
+
+	if ((pw = getpwnam(s)) != NULL) {
+		*uid = pw->pw_uid;
+		if (*uid == UID_MAX)
+			return -1;
+		return 0;
+	}
+	*uid = strtonum(s, 0, UID_MAX - 1, &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;
+		if (*gid == GID_MAX)
+			return -1;
+		return 0;
+	}
+	*gid = strtonum(s, 0, GID_MAX - 1, &errstr);
+	if (errstr)
+		return -1;
+	return 0;
+}
+
+static int
+match_identifier(const char *identifier, gid_t *groups, int ngroups,
+    uid_t euid, gid_t egid)
+{
+	int i;
+
+	if (identifier[0] == ':') {
+		gid_t rgid;
+		if (parsegid(identifier + 1, &rgid) == -1)
+			return 0;
+		for (i = 0; i < ngroups; i++) {
+			if (rgid == groups[i] && egid == rgid)
+				break;
+		}
+		if (i == ngroups)
+			return 0;
+	} else if (uidcheck(identifier, euid) != 0)
+		return 0;
+
+	return 1;
+}
+
+const struct got_error *
+gotd_auth_check(struct gotd_access_rule_list *rules, const char *repo_name,
+    gid_t *groups, int ngroups, uid_t euid, gid_t egid,
+    int required_auth)
+{
+	struct gotd_access_rule *rule;
+	enum gotd_access access = GOTD_ACCESS_DENIED;
+
+	STAILQ_FOREACH(rule, rules, entry) {
+		if (!match_identifier(rule->identifier, groups, ngroups,
+		    euid, egid))
+			continue;
+
+		access = rule->access;
+		if (rule->access == GOTD_ACCESS_PERMITTED &&
+		    (rule->authorization & required_auth) != required_auth)
+			access = GOTD_ACCESS_DENIED;
+	}
+
+	if (access == GOTD_ACCESS_DENIED)
+		return got_error_set_errno(EACCES, repo_name);
+
+	if (access == GOTD_ACCESS_PERMITTED)
+		return NULL;
+
+	/* should not happen, this would be a bug */
+	return got_error_msg(GOT_ERR_NOT_IMPL, "bad access rule");
+}
blob - 32fecfd55cde803ce2b92b4419940ace84bfd9a0
blob + 526949eca1f9f3357614b701fd37f29764969911
--- gotd/gotd.c
+++ gotd/gotd.c
@@ -1,5 +1,6 @@
 /*
  * Copyright (c) 2022 Stefan Sperling <stsp@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
@@ -58,6 +59,7 @@
 
 #include "gotd.h"
 #include "log.h"
+#include "auth.h"
 #include "repo_read.h"
 #include "repo_write.h"
 
@@ -543,6 +545,24 @@ static struct gotd_child_proc *
 	return NULL;
 }
 
+static struct gotd_repo *
+find_repo_by_name(const char *repo_name)
+{
+	struct gotd_repo *repo;
+	size_t namelen;
+
+	TAILQ_FOREACH(repo, &gotd.repos, entry) {
+		namelen = strlen(repo->name);
+		if (strncmp(repo->name, repo_name, namelen) != 0)
+			continue;
+		if (repo_name[namelen] == '\0' ||
+		    strcmp(&repo_name[namelen], ".git") == 0)
+			return repo;
+	}
+
+	return NULL;
+}
+
 static struct gotd_child_proc *
 find_proc_by_repo_name(enum gotd_procid proc_id, const char *repo_name)
 {
@@ -586,6 +606,7 @@ forward_list_refs_request(struct gotd_client *client, 
 	const struct got_error *err;
 	struct gotd_imsg_list_refs ireq;
 	struct gotd_imsg_list_refs_internal ilref;
+	struct gotd_repo *repo = NULL;
 	struct gotd_child_proc *proc = NULL;
 	size_t datalen;
 	int fd = -1;
@@ -605,6 +626,14 @@ forward_list_refs_request(struct gotd_client *client, 
 		err = ensure_client_is_not_writing(client);
 		if (err)
 			return err;
+		repo = find_repo_by_name(ireq.repo_name);
+		if (repo == NULL)
+			return got_error(GOT_ERR_NOT_GIT_REPO);
+		err = gotd_auth_check(&repo->rules, repo->name,
+		    gotd.groups, gotd.ngroups, client->euid, client->egid,
+		    GOTD_AUTH_READ);
+		if (err)
+			return err;
 		client->repo_read = find_proc_by_repo_name(PROC_REPO_READ,
 		    ireq.repo_name);
 		if (client->repo_read == NULL)
@@ -613,6 +642,14 @@ forward_list_refs_request(struct gotd_client *client, 
 		err = ensure_client_is_not_reading(client);
 		if (err)
 			return err;
+		repo = find_repo_by_name(ireq.repo_name);
+		if (repo == NULL)
+			return got_error(GOT_ERR_NOT_GIT_REPO);
+		err = gotd_auth_check(&repo->rules, repo->name,
+		    gotd.groups, gotd.ngroups, client->euid, client->egid,
+		    GOTD_AUTH_READ | GOTD_AUTH_WRITE);
+		if (err)
+			return err;
 		client->repo_write = find_proc_by_repo_name(PROC_REPO_WRITE,
 		    ireq.repo_name);
 		if (client->repo_write == NULL)
@@ -2060,8 +2097,7 @@ main(int argc, char **argv)
 	const char *confpath = GOTD_CONF_PATH;
 	char *argv0 = argv[0];
 	char title[2048];
-	gid_t groups[NGROUPS_MAX + 1];
-	int ngroups = NGROUPS_MAX + 1;
+	int ngroups = NGROUPS_MAX;
 	struct passwd *pw = NULL;
 	struct group *gr = NULL;
 	char *repo_path = NULL;
@@ -2131,10 +2167,11 @@ main(int argc, char **argv)
 		    getprogname(), pw->pw_name, getprogname());
 	}
 
-	if (getgrouplist(pw->pw_name, pw->pw_gid, groups, &ngroups) == -1)
+	if (getgrouplist(pw->pw_name, pw->pw_gid, gotd.groups, &ngroups) == -1)
 		log_warnx("group membership list truncated");
+	gotd.ngroups = ngroups;
 
-	gr = match_group(groups, ngroups, gotd.unix_group_name);
+	gr = match_group(gotd.groups, ngroups, gotd.unix_group_name);
 	if (gr == NULL) {
 		fatalx("cannot start %s: the user running %s "
 		    "must be a secondary member of group %s",
blob - /dev/null
blob + 480be955e04852bc1940c62233137deaeebcaedf (mode 644)
--- /dev/null
+++ gotd/auth.h
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2022 Stefan Sperling <stsp@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.
+ */
+
+const struct got_error *
+gotd_auth_check(struct gotd_access_rule_list *rules, const char *repo_name,
+    gid_t *groups, int ngroups, uid_t euid, gid_t egid, int required_auth);
blob - a684a594c65dd5e7d94771ee6dd42c488c2abe63
blob + 4ba88670ecbb2552a329c63ded99527718ffdca1
--- gotd/gotd.conf.5
+++ gotd/gotd.conf.5
@@ -72,6 +72,14 @@ to function.
 At least one repository context must exist for
 .Xr gotd 8
 to function.
+For each repository, access rules must be configured using the
+.Ic permit
+and
+.Ic deny
+configuration directives.
+Multiple access rules can be specified, and the last matching rule
+determines the action taken.
+If no rule matches, access to the repository is denied.
 .Pp
 A repository context is declared with a unique
 .Ar name ,
@@ -99,8 +107,32 @@ The available repository configuration directives are 
 .Pp
 The available repository configuration directives are as follows:
 .Bl -tag -width Ds
+.It Ic deny Ar identity
+Deny repository access to users with the username
+.Ar identity .
+Group names may be matched by prepending a colon
+.Pq Sq \&:
+to
+.Ar identity .
+Numeric IDs are also accepted.
 .It Ic path Ar path
 Set the path to the Git repository.
+.It Ic permit Ar mode Ar identity
+Permit repository access to users with the username
+.Ar identity .
+The
+.Ar mode
+argument must be set to either
+.Ic ro
+for read-only access,
+or
+.Ic rw
+for read-write access.
+Group names may be matched by prepending a colon
+.Pq Sq \&:
+to
+.Ar identity .
+Numeric IDs are also accepted.
 .El
 .Sh FILES
 .Bl -tag -width Ds -compact
@@ -118,12 +150,18 @@ repository "src" {
 # This repository can be accessed via ssh://user@example.com/src
 repository "src" {
 	path "/var/git/src.git"
+	permit rw flan_hacker
+	permit rw :developers
+	permit ro anonymous
 }
 
 # This repository can be accessed via
 # ssh://user@example.com/openbsd/ports
 repository "openbsd/ports" {
 	path "/var/git/ports.git"
+	permit rw :porters
+	permit ro anonymous
+	deny flan_hacker
 }
 .Ed
 .Sh SEE ALSO
blob - 6654c7116a6483e7457c581646809a9e5b09c6ae
blob + fa1f5d66ce3b4c6691a931b445425921470634a2
--- gotd/gotd.h
+++ gotd/gotd.h
@@ -54,11 +54,31 @@ struct gotd_repo {
 	size_t nhelpers;
 };
 
+enum gotd_access {
+	GOTD_ACCESS_PERMITTED = 1,
+	GOTD_ACCESS_DENIED
+};
+
+struct gotd_access_rule {
+	STAILQ_ENTRY(gotd_access_rule) entry;
+
+	enum gotd_access access;
+
+	int authorization;
+#define GOTD_AUTH_READ		0x1
+#define GOTD_AUTH_WRITE		0x2
+
+	char *identifier;
+};
+STAILQ_HEAD(gotd_access_rule_list, gotd_access_rule);
+
 struct gotd_repo {
 	TAILQ_ENTRY(gotd_repo)	 entry;
 
 	char name[NAME_MAX];
 	char path[PATH_MAX];
+
+	struct gotd_access_rule_list rules;
 };
 TAILQ_HEAD(gotd_repolist, gotd_repo);
 
@@ -97,6 +117,8 @@ struct gotd {
 	struct event pause;
 	struct gotd_child_proc *procs;
 	int nprocs;
+	gid_t groups[NGROUPS_MAX];
+	int ngroups;
 };
 
 enum gotd_imsg_type {
blob - 5ccdb6be0bc1138dacf2aa6282eca1b79c9bba8f
blob + 6e9284e02d242f34453195f8acfce937633b6ae7
--- gotd/parse.y
+++ gotd/parse.y
@@ -98,7 +98,7 @@ typedef struct {
 
 %}
 
-%token	PATH ERROR ON UNIX_SOCKET UNIX_GROUP USER REPOSITORY
+%token	PATH ERROR ON UNIX_SOCKET UNIX_GROUP USER REPOSITORY PERMIT DENY
 
 %token	<v.string>	STRING
 %token	<v.number>	NUMBER
@@ -221,6 +221,25 @@ repoopts1	: PATH STRING {
 			}
 			free($2);
 		}
+		| PERMIT RO STRING {
+			if (gotd_proc_id == PROC_GOTD) {
+				conf_new_access_rule(new_repo,
+				    GOTD_ACCESS_PERMITTED, GOTD_AUTH_READ, $3);
+			}
+		}
+		| PERMIT RW STRING {
+			if (gotd_proc_id == PROC_GOTD) {
+				conf_new_access_rule(new_repo,
+				    GOTD_ACCESS_PERMITTED,
+				    GOTD_AUTH_READ | GOTD_AUTH_WRITE, $3);
+			}
+		}
+		| DENY STRING {
+			if (gotd_proc_id == PROC_GOTD) {
+				conf_new_access_rule(new_repo,
+				    GOTD_ACCESS_DENIED, 0, $2);
+			}
+		}
 		;
 
 repoopts2	: repoopts2 repoopts1 nl
@@ -268,9 +287,13 @@ lookup(char *s)
 {
 	/* This has to be sorted always. */
 	static const struct keywords keywords[] = {
+		{ "deny",			DENY },
 		{ "on",				ON },
 		{ "path",			PATH },
+		{ "permit",			PERMIT },
 		{ "repository",			REPOSITORY },
+		{ "ro",				RO },
+		{ "rw",				RW },
 		{ "unix_group",			UNIX_GROUP },
 		{ "unix_socket",		UNIX_SOCKET },
 		{ "user",			USER },
@@ -667,6 +690,8 @@ conf_new_repo(const char *name)
 	if (repo == NULL)
 		fatalx("%s: calloc", __func__);
 
+	STAILQ_INIT(&repo->rules);
+
 	if (strlcpy(repo->name, name, sizeof(repo->name)) >=
 	    sizeof(repo->name))
 		fatalx("%s: strlcpy", __func__);
@@ -677,6 +702,23 @@ int
 	return repo;
 };
 
+static void
+conf_new_access_rule(struct gotd_repo *repo, enum gotd_access access,
+    int authorization, char *identifier)
+{
+	struct gotd_access_rule *rule;
+
+	rule = calloc(1, sizeof(*rule));
+	if (rule == NULL)
+		fatal("calloc");
+
+	rule->access = access;
+	rule->authorization = authorization;
+	rule->identifier = identifier;
+
+	STAILQ_INSERT_TAIL(&repo->rules, rule, entry);
+}
+
 int
 symset(const char *nam, const char *val, int persist)
 {
blob - 0ac2352fd9d6c39216dc6fe24a959fdd62f17cbe
blob + 773e9c0dfd3c2e287025d41ffed9f3749b380941
--- regress/gotd/Makefile
+++ regress/gotd/Makefile
@@ -35,16 +35,28 @@ start_gotd: ensure_root
 		false; \
 	fi
 
-start_gotd: ensure_root
+start_gotd_ro: ensure_root
 	@echo 'unix_socket "$(GOTD_SOCK)"' > $(PWD)/gotd.conf
 	@echo "unix_group $(GOTD_GROUP)" >> $(PWD)/gotd.conf
 	@echo "user $(GOTD_USER)" >> $(PWD)/gotd.conf
 	@echo 'repository "test-repo" {' >> $(PWD)/gotd.conf
 	@echo '    path "$(GOTD_TEST_REPO)"' >> $(PWD)/gotd.conf
+	@echo '    permit ro $(GOTD_DEVUSER)' >> $(PWD)/gotd.conf
 	@echo "}" >> $(PWD)/gotd.conf
 	@$(GOTD_TRAP); $(GOTD_START_CMD)
 	@$(GOTD_TRAP); sleep .5
 
+start_gotd_rw: ensure_root
+	@echo 'unix_socket "$(GOTD_SOCK)"' > $(PWD)/gotd.conf
+	@echo "unix_group $(GOTD_GROUP)" >> $(PWD)/gotd.conf
+	@echo "user $(GOTD_USER)" >> $(PWD)/gotd.conf
+	@echo 'repository "test-repo" {' >> $(PWD)/gotd.conf
+	@echo '    path "$(GOTD_TEST_REPO)"' >> $(PWD)/gotd.conf
+	@echo '    permit rw $(GOTD_DEVUSER)' >> $(PWD)/gotd.conf
+	@echo "}" >> $(PWD)/gotd.conf
+	@$(GOTD_TRAP); $(GOTD_START_CMD)
+	@$(GOTD_TRAP); sleep .5
+
 prepare_test_repo: ensure_root
 	@chown ${GOTD_USER} "${GOTD_TEST_REPO}"
 	@su -m ${GOTD_USER} -c 'env $(GOTD_TEST_ENV) sh ./prepare_test_repo.sh'
@@ -53,19 +65,19 @@ test_repo_read: prepare_test_repo start_gotd
 	@chown ${GOTD_USER} "${GOTD_TEST_REPO}"
 	@su -m ${GOTD_USER} -c 'env $(GOTD_TEST_ENV) sh ./prepare_test_repo.sh 1'
 
-test_repo_read: prepare_test_repo start_gotd
+test_repo_read: prepare_test_repo start_gotd_ro
 	@-$(GOTD_TRAP); su ${GOTD_TEST_USER} -c \
 		'env $(GOTD_TEST_ENV) sh ./repo_read.sh'
 	@$(GOTD_STOP_CMD) 2>/dev/null
 	@su -m ${GOTD_USER} -c 'env $(GOTD_TEST_ENV) sh ./check_test_repo.sh'
 
-test_repo_write: prepare_test_repo start_gotd
+test_repo_write: prepare_test_repo start_gotd_rw
 	@-$(GOTD_TRAP); su ${GOTD_TEST_USER} -c \
 		'env $(GOTD_TEST_ENV) sh ./repo_write.sh'
 	@$(GOTD_STOP_CMD) 2>/dev/null
 	@su -m ${GOTD_USER} -c 'env $(GOTD_TEST_ENV) sh ./check_test_repo.sh'
 
-test_repo_write_empty: prepare_test_repo_empty start_gotd
+test_repo_write_empty: prepare_test_repo_empty start_gotd_rw
 	@-$(GOTD_TRAP); su ${GOTD_TEST_USER} -c \
 		'env $(GOTD_TEST_ENV) sh ./repo_write_empty.sh'
 	@$(GOTD_STOP_CMD) 2>/dev/null
blob - 82df572967b423dd79abedbe9e1315e14be9b6bf
blob + b1f71b3d833259a85937508a71f30bc99848279a
--- regress/gotd/repo_read.sh
+++ regress/gotd/repo_read.sh
@@ -70,5 +70,52 @@ test_parseargs "$@"
 	test_done "$testroot" "$ret"
 }
 
+test_send_to_read_only_repo() {
+	local testroot=`test_init send_to_read_only_repo 1`
+
+	ls -R ${GOTD_TEST_REPO} > $testroot/repo-list.before
+
+	got clone -q ${GOTD_TEST_REPO_URL} $testroot/repo-clone
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got clone failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	got checkout -q $testroot/repo-clone $testroot/wt >/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got checkout failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	mkdir $testroot/wt/psi
+	echo "new" > $testroot/wt/psi/new
+	(cd $testroot/wt && got add psi/new > /dev/null)
+	echo "more alpha" >> $testroot/wt/alpha
+	(cd $testroot/wt && got commit -m 'make changes' > /dev/null)
+
+	got send -q -r $testroot/repo-clone 2>$testroot/stderr
+	ret=$?
+	if [ $ret -eq 0 ]; then
+		echo "got send succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo 'got-send-pack: test-repo: Permission denied' \
+		> $testroot/stderr.expected
+	echo 'got: could not send pack file' >> $testroot/stderr.expected
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+	fi
+	test_done "$testroot" "$ret"
+}
+
 test_parseargs "$@"
 run_test test_clone_basic
+run_test test_send_to_read_only_repo