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

From:
Stefan Sperling <stsp@stsp.name>
Subject:
fix gotd group auth
To:
gameoftrees@openbsd.org
Date:
Thu, 17 Nov 2022 10:03:36 +0100

Download raw body.

gotd group auth is broken. It matches against the groups of
the user running gotd instead of the user which is connecting
to the socket :-) Fix this and add tests for various cases
where gotd should deny repository read access.

ok?

diff refs/heads/main refs/heads/gotd-auth-group
commit - c2a4f618fa3c9683fbb5f384b117f7e45a041122
commit + c8b61813bb25b1170687311d5deba5f694b21a93
blob - b8b141d19d580a01c1cc68f758b798a5f2947b11
blob + ce2c2df55ff1550b6443b71f5873da9002df3eac
--- gotd/auth.c
+++ gotd/auth.c
@@ -21,6 +21,7 @@
 
 #include <errno.h>
 #include <event.h>
+#include <grp.h>
 #include <limits.h>
 #include <pwd.h>
 #include <grp.h>
@@ -29,10 +30,12 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <imsg.h>
+#include <unistd.h>
 
 #include "got_error.h"
 
 #include "gotd.h"
+#include "log.h"
 #include "auth.h"
 
 static int
@@ -93,8 +96,10 @@ match_identifier(const char *identifier, gid_t *groups
 		gid_t rgid;
 		if (parsegid(identifier + 1, &rgid) == -1)
 			return 0;
+		if (rgid == egid)
+			return 1;
 		for (i = 0; i < ngroups; i++) {
-			if (rgid == groups[i] && egid == rgid)
+			if (rgid == groups[i])
 				break;
 		}
 		if (i == ngroups)
@@ -107,12 +112,22 @@ gotd_auth_check(struct gotd_access_rule_list *rules, c
 
 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)
+    uid_t euid, gid_t egid, int required_auth)
 {
 	struct gotd_access_rule *rule;
 	enum gotd_access access = GOTD_ACCESS_DENIED;
+	struct passwd *pw;
+	gid_t groups[NGROUPS_MAX];
+	int ngroups;
 
+	errno = 0; /* allow us to tell apart getpwuid does not clear */
+	pw = getpwuid(euid);
+	if (pw == NULL)
+		return got_error_from_errno("getpwuid");
+
+	if (getgrouplist(pw->pw_name, pw->pw_gid, groups, &ngroups) == -1)
+		log_warnx("group membership list truncated");
+
 	STAILQ_FOREACH(rule, rules, entry) {
 		if (!match_identifier(rule->identifier, groups, ngroups,
 		    euid, egid))
blob - 480be955e04852bc1940c62233137deaeebcaedf
blob + 506f5e8b84ac2182f7114a5f4569b3b8d6963cc2
--- gotd/auth.h
+++ gotd/auth.h
@@ -16,4 +16,4 @@ gotd_auth_check(struct gotd_access_rule_list *rules, c
 
 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);
+    uid_t euid, gid_t egid, int required_auth);
blob - 526949eca1f9f3357614b701fd37f29764969911
blob + c9a5e9898ab184ee0b9168d135e56c9ae529e183
--- gotd/gotd.c
+++ gotd/gotd.c
@@ -630,8 +630,7 @@ forward_list_refs_request(struct gotd_client *client, 
 		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);
+		    client->euid, client->egid, GOTD_AUTH_READ);
 		if (err)
 			return err;
 		client->repo_read = find_proc_by_repo_name(PROC_REPO_READ,
@@ -645,9 +644,8 @@ forward_list_refs_request(struct gotd_client *client, 
 		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);
+		err = gotd_auth_check(&repo->rules, repo->name, 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,
@@ -2097,6 +2095,7 @@ main(int argc, char **argv)
 	const char *confpath = GOTD_CONF_PATH;
 	char *argv0 = argv[0];
 	char title[2048];
+	gid_t groups[NGROUPS_MAX];
 	int ngroups = NGROUPS_MAX;
 	struct passwd *pw = NULL;
 	struct group *gr = NULL;
@@ -2167,11 +2166,10 @@ main(int argc, char **argv)
 		    getprogname(), pw->pw_name, getprogname());
 	}
 
-	if (getgrouplist(pw->pw_name, pw->pw_gid, gotd.groups, &ngroups) == -1)
+	if (getgrouplist(pw->pw_name, pw->pw_gid, groups, &ngroups) == -1)
 		log_warnx("group membership list truncated");
-	gotd.ngroups = ngroups;
 
-	gr = match_group(gotd.groups, ngroups, gotd.unix_group_name);
+	gr = match_group(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 - fa1f5d66ce3b4c6691a931b445425921470634a2
blob + 1137c32a92e87b805d1195b4b06d153a9a53d1fa
--- gotd/gotd.h
+++ gotd/gotd.h
@@ -117,8 +117,6 @@ struct gotd {
 	struct event pause;
 	struct gotd_child_proc *procs;
 	int nprocs;
-	gid_t groups[NGROUPS_MAX];
-	int ngroups;
 };
 
 enum gotd_imsg_type {
blob - 773e9c0dfd3c2e287025d41ffed9f3749b380941
blob + cd20ee9bf8631fcd757767edf78df5f410e56161
--- regress/gotd/Makefile
+++ regress/gotd/Makefile
@@ -1,4 +1,7 @@
-REGRESS_TARGETS=test_repo_read test_repo_write test_repo_write_empty
+REGRESS_TARGETS=test_repo_read test_repo_read_group \
+	test_repo_read_denied_user test_repo_read_denied_group \
+	test_repo_read_bad_user test_repo_read_bad_group \
+	test_repo_write test_repo_write_empty
 NOOBJ=Yes
 
 .PHONY: ensure_root prepare_test_repo check_test_repo start_gotd
@@ -46,6 +49,67 @@ start_gotd_rw: ensure_root
 	@$(GOTD_TRAP); $(GOTD_START_CMD)
 	@$(GOTD_TRAP); sleep .5
 
+start_gotd_ro_group: 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
+
+# try a permit rule followed by a deny rule; last matched rule wins
+start_gotd_ro_denied_user: 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 '    deny $(GOTD_DEVUSER)' >> $(PWD)/gotd.conf
+	@echo "}" >> $(PWD)/gotd.conf
+	@$(GOTD_TRAP); $(GOTD_START_CMD)
+	@$(GOTD_TRAP); sleep .5
+
+# try a permit rule followed by a deny rule; last matched rule wins
+start_gotd_ro_denied_group: 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 '    deny :$(GOTD_DEVUSER)' >> $(PWD)/gotd.conf
+	@echo "}" >> $(PWD)/gotd.conf
+	@$(GOTD_TRAP); $(GOTD_START_CMD)
+	@$(GOTD_TRAP); sleep .5
+
+# $GOTD_DEVUSER should not equal $GOTD_USER
+start_gotd_ro_bad_user: 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_USER)' >> $(PWD)/gotd.conf
+	@echo "}" >> $(PWD)/gotd.conf
+	@$(GOTD_TRAP); $(GOTD_START_CMD)
+	@$(GOTD_TRAP); sleep .5
+
+# $GOTD_DEVUSER should not be in group wheel
+start_gotd_ro_bad_group: 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 :wheel' >> $(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
@@ -71,6 +135,36 @@ test_repo_write: prepare_test_repo start_gotd_rw
 	@$(GOTD_STOP_CMD) 2>/dev/null
 	@su -m ${GOTD_USER} -c 'env $(GOTD_TEST_ENV) sh ./check_test_repo.sh'
 
+test_repo_read_group: prepare_test_repo start_gotd_ro_group
+	@-$(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_read_denied_user: prepare_test_repo start_gotd_ro_denied_user
+	@-$(GOTD_TRAP); su ${GOTD_TEST_USER} -c \
+		'env $(GOTD_TEST_ENV) sh ./repo_read_access_denied.sh'
+	@$(GOTD_STOP_CMD) 2>/dev/null
+	@su -m ${GOTD_USER} -c 'env $(GOTD_TEST_ENV) sh ./check_test_repo.sh'
+
+test_repo_read_denied_group: prepare_test_repo start_gotd_ro_denied_group
+	@-$(GOTD_TRAP); su ${GOTD_TEST_USER} -c \
+		'env $(GOTD_TEST_ENV) sh ./repo_read_access_denied.sh'
+	@$(GOTD_STOP_CMD) 2>/dev/null
+	@su -m ${GOTD_USER} -c 'env $(GOTD_TEST_ENV) sh ./check_test_repo.sh'
+
+test_repo_read_bad_user: prepare_test_repo start_gotd_ro_bad_user
+	@-$(GOTD_TRAP); su ${GOTD_TEST_USER} -c \
+		'env $(GOTD_TEST_ENV) sh ./repo_read_access_denied.sh'
+	@$(GOTD_STOP_CMD) 2>/dev/null
+	@su -m ${GOTD_USER} -c 'env $(GOTD_TEST_ENV) sh ./check_test_repo.sh'
+
+test_repo_read_bad_group: prepare_test_repo start_gotd_ro_bad_group
+	@-$(GOTD_TRAP); su ${GOTD_TEST_USER} -c \
+		'env $(GOTD_TEST_ENV) sh ./repo_read_access_denied.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_rw
 	@-$(GOTD_TRAP); su ${GOTD_TEST_USER} -c \
 		'env $(GOTD_TEST_ENV) sh ./repo_write.sh'
blob - /dev/null
blob + c8b4fc889c65a723539f0957fff96fda7cd7d32b (mode 644)
--- /dev/null
+++ regress/gotd/repo_read_access_denied.sh
@@ -0,0 +1,47 @@
+#!/bin/sh
+#
+# 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.
+
+. ../cmdline/common.sh
+. ./common.sh
+
+test_clone_basic_access_denied() {
+	local testroot=`test_init clone_basic_access_denied 1`
+
+	cp -r ${GOTD_TEST_REPO} $testroot/repo-copy
+
+	got clone -q ${GOTD_TEST_REPO_URL} $testroot/repo-clone \
+		2> $testroot/stderr
+	ret=$?
+	if [ $ret -eq 0 ]; then
+		echo "got clone succeeded unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	# Verify that the clone operation failed.
+	echo 'got-fetch-pack: test-repo: Permission denied' \
+		> $testroot/stderr.expected
+	echo 'got: fetch failed' >> $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_access_denied