From: Stefan Sperling Subject: gotd auth rules To: gameoftrees@openbsd.org Date: Tue, 15 Nov 2022 16:59:18 +0100 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 + * Copyright (c) 2015 Ted Unangst + * + * 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 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 + * Copyright (c) 2015 Ted Unangst * * 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 + * + * 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 STRING %token 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