commit - 86b188ee113cde1b53e1d3544b40ce80ab7767a7
commit + 0ccf3acb6c3004ac41b46ad931024da1f4ea0e3e
blob - 0d22c3ab2c4d40606c11c44bd82449f367e2a9bf
blob + c10b6a5df87a4c719d3bb926aff2316341052ac3
--- gotd/Makefile
+++ gotd/Makefile
.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
+/*
+ * 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
/*
* 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
#include "gotd.h"
#include "log.h"
+#include "auth.h"
#include "repo_read.h"
#include "repo_write.h"
gotd_shutdown();
/* NOTREACHED */
+ 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;
}
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;
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)
return got_error(GOT_ERR_NOT_GIT_REPO);
} else {
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,
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;
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
+/*
+ * 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
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 ,
.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
# 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
int pipe[2];
struct gotd_imsgev iev;
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);
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
%}
-%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
}
}
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
{
/* 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 },
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__);
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
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'
@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
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