"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Stefan Sperling <stsp@stsp.name>
Subject:
gotd protected references
To:
gameoftrees@openbsd.org
Date:
Fri, 31 Mar 2023 22:06:23 +0200

Download raw body.

Thread
This patch adds a "protect" directive to gotd.conf which can be used
to forbid 'got send -f' on selected branches/tags.

There will always be accidents with 'got send -f' at some point in a
project's life cycle so gotd should provide safeguards against this.

ok?

-----------------------------------------------
 add support for protecting references against 'got send -f' to gotd
 
diff e9e0377f452e9d3f600011e0714cc6c779f10bab 18abd72047de1eac9a8ced01a253a737f8ae6d81
commit - e9e0377f452e9d3f600011e0714cc6c779f10bab
commit + 18abd72047de1eac9a8ced01a253a737f8ae6d81
blob - 38e4349026233f676c860654e1ebed2cc32c13f3
blob + 02cc7ea7b999fff68dd0fd76a0015622e5c99b68
--- gotctl/gotctl.c
+++ gotctl/gotctl.c
@@ -33,6 +33,7 @@
 
 #include "got_error.h"
 #include "got_version.h"
+#include "got_path.h"
 
 #include "got_lib_gitproto.h"
 
blob - 794c48b1569cbd4214a7d34d9e2e81ac05ff789c
blob + aa23d5a751490771ef96a646b52a1a40f8332ade
--- gotd/gotd.c
+++ gotd/gotd.c
@@ -1700,6 +1700,7 @@ main(int argc, char **argv)
 	enum gotd_procid proc_id = PROC_GOTD;
 	struct event evsigint, evsigterm, evsighup, evsigusr1;
 	int *pack_fds = NULL, *temp_fds = NULL;
+	struct gotd_repo *repo = NULL;
 
 	log_init(1, LOG_DAEMON); /* Log to stderr until daemonized. */
 
@@ -1897,7 +1898,11 @@ main(int argc, char **argv)
 			err(1, "pledge");
 #endif
 		apply_unveil_repo_readonly(repo_path);
-		repo_write_main(title, repo_path, pack_fds, temp_fds);
+		repo = gotd_find_repo_by_path(repo_path, &gotd);
+		if (repo == NULL)
+			fatalx("no repository for path %s", repo_path);
+		repo_write_main(title, repo_path, pack_fds, temp_fds,
+		    &repo->protected_refs, &repo->protected_namespaces);
 		/* NOTREACHED */
 		exit(0);
 	default:
blob - 42253ba60317aec22ed6f18dd8917d823ce845ed
blob + f15c866dfcd0adaa409ac969e215043f3cb67296
--- gotd/gotd.conf.5
+++ gotd/gotd.conf.5
@@ -172,7 +172,60 @@ Numeric IDs are also accepted.
 to
 .Ar identity .
 Numeric IDs are also accepted.
+.It Ic protect Brq Ar ...
+The
+.Cm protect
+directive may be used to protect existing references in a repository
+from being overwritten by potentially destructive client-side commands,
+such as
+.Cm got send -f
+and
+.Cm git push -f .
+.Pp
+To build a set of protected references, multiple
+.Ic protect
+directives may be specified per repository and
+multiple
+.Ic protect
+directive parameters may be specified within curly braces.
+.Pp
+The available
+.Cm protect
+parameters are as follows:
+.Pp
+.Bl -tag -width Ds
+.It Ic branch Ar name
+Protect the named branch.
+The
+.Ar name
+will be looked up in the
+.Dq refs/heads/
+reference namespace.
+.It Ic reference Ar name
+Protect the named reference.
+The reference
+.Ar name
+must be absolute, starting with
+.Dq refs/ .
+.It Ic namespace Ar namespace
+Protect all references in a given reference namespace.
+The 
+.Ar namespace
+must be absolute, starting with
+.Dq refs/ .
+While new references may be created in a protected namespace, any attempts
+to modify or delete existing references from the namespace will be denied.
+.Pp
+The special namespaces
+.Dq refs/got/
+and
+.Dq refs/remotes/
+do not need to be listed in
+.Nm .
+These namespaces are always protected and even attempts to create new
+references in these namespaces will always be denied.
 .El
+.El
 .Sh FILES
 .Bl -tag -width Ds -compact
 .It Pa /etc/gotd.conf
@@ -194,6 +247,9 @@ repository "src" {
 	permit rw flan_hacker
 	permit rw :developers
 	permit ro anonymous
+
+	protect branch "main"
+	protect namespace "refs/tags/"
 }
 
 # This repository can be accessed via
@@ -203,6 +259,11 @@ repository "openbsd/ports" {
 	permit rw :porters
 	permit ro anonymous
 	deny flan_hacker
+
+	protect {
+		branch "main"
+		namespace "refs/tags/"
+	}
 }
 
 # Use a larger request timeout value:
blob - 1f9b40a97a0e8ed68f190efc0abc407e5fbc5fde
blob + c21f06879b2872e426dcfd05ea08d1ccf1e7502c
--- gotd/gotd.h
+++ gotd/gotd.h
@@ -85,6 +85,8 @@ struct gotd_repo {
 	char path[PATH_MAX];
 
 	struct gotd_access_rule_list rules;
+	struct got_pathlist_head protected_refs;
+	struct got_pathlist_head protected_namespaces;
 };
 TAILQ_HEAD(gotd_repolist, gotd_repo);
 
@@ -448,6 +450,7 @@ struct gotd_repo *gotd_find_repo_by_name(const char *,
 
 int parse_config(const char *, enum gotd_procid, struct gotd *, int);
 struct gotd_repo *gotd_find_repo_by_name(const char *, struct gotd *);
+struct gotd_repo *gotd_find_repo_by_path(const char *, struct gotd *);
 
 /* imsg.c */
 const struct got_error *gotd_imsg_flush(struct imsgbuf *);
blob - 3813dd6071aae8355cf41ade517cd479db0cd5af
blob + 86b16615f22802ae9f3a4f249265458eb1392651
--- gotd/imsg.c
+++ gotd/imsg.c
@@ -30,6 +30,7 @@
 #include <unistd.h>
 
 #include "got_error.h"
+#include "got_path.h"
 
 #include "got_lib_poll.h"
 
blob - a0953584fa86b2eef4d45cc9ccb6d5823eb55f00
blob + b51f18490312837f6627991cf323dc1e5bc2d7a6
--- gotd/listen.c
+++ gotd/listen.c
@@ -34,6 +34,7 @@
 #include <unistd.h>
 
 #include "got_error.h"
+#include "got_path.h"
 
 #include "gotd.h"
 #include "log.h"
blob - b7d7e708dc1650571a862cb8f93f26798d1f1037
blob + 09cffe648d2c60f82fabdd79d7908f55dd8e9a90
--- gotd/parse.y
+++ gotd/parse.y
@@ -44,6 +44,7 @@
 
 #include "got_error.h"
 #include "got_path.h"
+#include "got_reference.h"
 
 #include "log.h"
 #include "gotd.h"
@@ -92,6 +93,9 @@ static enum gotd_procid		 gotd_proc_id;
 static struct gotd_repo		*conf_new_repo(const char *);
 static void			 conf_new_access_rule(struct gotd_repo *,
 				    enum gotd_access, int, char *);
+static int			 conf_protect_ref(struct gotd_repo *, char *);
+static int			 conf_protect_namespace(struct gotd_repo *, char *);
+static int			 conf_protect_branch(struct gotd_repo *, char *);
 static enum gotd_procid		 gotd_proc_id;
 
 typedef struct {
@@ -107,6 +111,7 @@ typedef struct {
 
 %token	PATH ERROR LISTEN ON USER REPOSITORY PERMIT DENY
 %token	RO RW CONNECTION LIMIT REQUEST TIMEOUT
+%token	PROTECT NAMESPACE BRANCH REFERENCE
 
 %token	<v.string>	STRING
 %token	<v.number>	NUMBER
@@ -229,6 +234,43 @@ repository	: REPOSITORY STRING {
 		}
 		;
 
+protect		: PROTECT '{' optnl protectflags_l '}'
+		| PROTECT protectflags
+
+protectflags_l	: protectflags optnl protectflags_l
+		| protectflags optnl
+		;
+
+protectflags	: REFERENCE STRING {
+			if (gotd_proc_id == PROC_GOTD ||
+			    gotd_proc_id == PROC_REPO_WRITE) {
+				if (conf_protect_ref(new_repo, $2)) {
+					free($2);
+					YYERROR;
+				}
+			}
+		}
+		| NAMESPACE STRING {
+			if (gotd_proc_id == PROC_GOTD ||
+			    gotd_proc_id == PROC_REPO_WRITE) {
+				if (conf_protect_namespace(new_repo, $2)) {
+					free($2);
+					YYERROR;
+				}
+				free($2);
+			}
+		}
+		| BRANCH STRING {
+			if (gotd_proc_id == PROC_GOTD ||
+			    gotd_proc_id == PROC_REPO_WRITE) {
+				if (conf_protect_branch(new_repo, $2)) {
+					free($2);
+					YYERROR;
+				}
+			}
+		}
+		;
+
 repository	: REPOSITORY STRING {
 			struct gotd_repo *repo;
 
@@ -241,7 +283,8 @@ repository	: REPOSITORY STRING {
 			}
 
 			if (gotd_proc_id == PROC_GOTD ||
-			    gotd_proc_id == PROC_AUTH) {
+			    gotd_proc_id == PROC_AUTH ||
+			    gotd_proc_id == PROC_REPO_WRITE) {
 				new_repo = conf_new_repo($2);
 			}
 			free($2);
@@ -251,7 +294,8 @@ repoopts1	: PATH STRING {
 
 repoopts1	: PATH STRING {
 			if (gotd_proc_id == PROC_GOTD ||
-			    gotd_proc_id == PROC_AUTH) {
+			    gotd_proc_id == PROC_AUTH ||
+			    gotd_proc_id == PROC_REPO_WRITE) {
 				if (!got_path_is_absolute($2)) {
 					yyerror("%s: path %s is not absolute",
 					    __func__, $2);
@@ -285,6 +329,7 @@ repoopts1	: PATH STRING {
 				    GOTD_ACCESS_DENIED, 0, $2);
 			}
 		}
+		| protect
 		;
 
 repoopts2	: repoopts2 repoopts1 nl
@@ -332,13 +377,17 @@ lookup(char *s)
 {
 	/* This has to be sorted always. */
 	static const struct keywords keywords[] = {
+		{ "branch",			BRANCH },
 		{ "connection",			CONNECTION },
 		{ "deny",			DENY },
 		{ "limit",			LIMIT },
 		{ "listen",			LISTEN },
+		{ "namespace",			NAMESPACE },
 		{ "on",				ON },
 		{ "path",			PATH },
 		{ "permit",			PERMIT },
+		{ "protect",			PROTECT },
+		{ "reference",			REFERENCE },
 		{ "repository",			REPOSITORY },
 		{ "request",			REQUEST },
 		{ "ro",				RO },
@@ -811,6 +860,8 @@ conf_new_repo(const char *name)
 		fatalx("%s: calloc", __func__);
 
 	STAILQ_INIT(&repo->rules);
+	TAILQ_INIT(&repo->protected_refs);
+	TAILQ_INIT(&repo->protected_namespaces);
 
 	if (strlcpy(repo->name, name, sizeof(repo->name)) >=
 	    sizeof(repo->name))
@@ -839,6 +890,82 @@ int
 	STAILQ_INSERT_TAIL(&repo->rules, rule, entry);
 }
 
+static int
+conf_protect_ref(struct gotd_repo *repo, char *refname)
+{
+	const struct got_error *error;
+
+	if (!got_ref_name_is_valid(refname)) {
+		yyerror("invalid reference name: %s", refname);
+		return -1;
+	}
+
+	if (strlen(refname) < 5 || strncmp(refname, "refs/", 5) != 0) {
+		yyerror("reference name must begin with \"refs/\": %s",
+		    refname);
+		return -1;
+	}
+
+	error = got_pathlist_insert(NULL, &repo->protected_refs,
+	    refname, NULL);
+	if (error) {
+		yyerror("got_pathlist_insert: %s", error->msg);
+		return -1;
+	}
+
+	return 0;
+}
+
+static int
+conf_protect_namespace(struct gotd_repo *repo, char *namespace)
+{
+	const struct got_error *error;
+	char *s;
+
+	got_path_strip_trailing_slashes(namespace);
+	if (!got_ref_name_is_valid(namespace)) {
+		yyerror("invalid reference namespace: %s", namespace);
+		return -1;
+	}
+
+	if (strlen(namespace) < 5 || strncmp(namespace, "refs/", 5) != 0) {
+		yyerror("reference namespace must begin with \"refs/\": %s",
+		    namespace);
+		return -1;
+	}
+
+	if (asprintf(&s, "%s/", namespace) == -1) {
+		yyerror("asprintf: %s", strerror(errno));
+		return -1;
+	}
+
+	error = got_pathlist_insert(NULL, &repo->protected_namespaces,
+	    s, NULL);
+	if (error) {
+		yyerror("got_pathlist_insert: %s", error->msg);
+		return -1;
+	}
+
+	return 0;
+}
+
+static int
+conf_protect_branch(struct gotd_repo *repo, char *branchname)
+{
+	char *refname;
+	int ret;
+
+	if (asprintf(&refname, "refs/heads/%s", branchname) == -1) {
+		yyerror("asprintf: %s", strerror(errno));
+		return -1;
+	}
+
+	ret = conf_protect_ref(repo, refname);
+	if (ret)
+		free(refname);
+	return ret;
+}
+
 int
 symset(const char *nam, const char *val, int persist)
 {
@@ -911,3 +1038,16 @@ gotd_find_repo_by_name(const char *repo_name, struct g
 
 	return NULL;
 }
+
+struct gotd_repo *
+gotd_find_repo_by_path(const char *repo_path, struct gotd *gotd)
+{
+	struct gotd_repo *repo;
+
+	TAILQ_FOREACH(repo, &gotd->repos, entry) {
+		if (strcmp(repo->path, repo_path) == 0)
+			return repo;
+	}
+
+	return NULL;
+}
blob - a4d19987c878bbc0be5a08026bc1ba88bf3a3f11
blob + 77af3223695f08a4c263ad27a9d0d0333b0307e6
--- gotd/repo_imsg.c
+++ gotd/repo_imsg.c
@@ -30,6 +30,7 @@
 
 #include "got_error.h"
 #include "got_object.h"
+#include "got_path.h"
 
 #include "got_lib_hash.h"
 
blob - 7775b1f5018ccb890406b37a3815e460b7cb8244
blob + b038586cc341ac1f637b059595471f402c3a54aa
--- gotd/repo_read.c
+++ gotd/repo_read.c
@@ -37,6 +37,7 @@
 #include "got_repository.h"
 #include "got_reference.h"
 #include "got_repository_admin.h"
+#include "got_path.h"
 
 #include "got_lib_delta.h"
 #include "got_lib_object.h"
blob - 558b5bdb3d78eb3e3f997f05a681c61ec2f73414
blob + ca79147f555269b4bb74bde78dfbef5e5a6b1e65
--- gotd/repo_write.c
+++ gotd/repo_write.c
@@ -69,6 +69,8 @@ static struct repo_write {
 	int *temp_fds;
 	int session_fd;
 	struct gotd_imsgev session_iev;
+	struct got_pathlist_head *protected_refs;
+	struct got_pathlist_head *protected_namespaces;
 } repo_write;
 
 struct gotd_ref_update {
@@ -329,6 +331,22 @@ recv_ref_update(struct imsg *imsg)
 }
 
 static const struct got_error *
+protect_ref(struct got_reference *ref, const char *refname)
+{
+	size_t len = strlen(refname);
+
+	if (len < 5 || strncmp("refs/", refname, 5) != 0) {
+		return got_error_fmt(GOT_ERR_BAD_REF_NAME,
+		    "reference '%s'", refname);
+	}
+
+	if (strncmp(refname, got_ref_get_name(ref), len) == 0)
+		return got_error_fmt(GOT_ERR_REF_PROTECTED, "%s", refname);
+
+	return NULL;
+}
+
+static const struct got_error *
 recv_ref_update(struct imsg *imsg)
 {
 	static const char zero_id[SHA1_DIGEST_LENGTH];
@@ -341,6 +359,7 @@ recv_ref_update(struct imsg *imsg)
 	struct got_object_id *id = NULL;
 	struct imsgbuf ibuf;
 	struct gotd_ref_update *ref_update = NULL;
+	struct got_pathlist_entry *pe;
 
 	log_debug("ref-update received");
 
@@ -397,6 +416,17 @@ recv_ref_update(struct imsg *imsg)
 		goto done;
 
 	if (!ref_update->ref_is_new) {
+		TAILQ_FOREACH(pe, repo_write.protected_namespaces, entry) {
+			err = protect_ref_namespace(ref, pe->path);
+			if (err)
+				goto done;
+		}
+		TAILQ_FOREACH(pe, repo_write.protected_refs, entry) {
+			err = protect_ref(ref, pe->path);
+			if (err)
+				goto done;
+		}
+
 		/*
 		 * Ensure the client's idea of this update is still valid.
 		 * At this point we can only return an error, to prevent
@@ -1437,7 +1467,9 @@ repo_write_main(const char *title, const char *repo_pa
 
 void
 repo_write_main(const char *title, const char *repo_path,
-    int *pack_fds, int *temp_fds)
+    int *pack_fds, int *temp_fds,
+    struct got_pathlist_head *protected_refs,
+    struct got_pathlist_head *protected_namespaces)
 {
 	const struct got_error *err = NULL;
 	struct repo_write_client *client = &repo_write_client;
@@ -1454,6 +1486,8 @@ repo_write_main(const char *title, const char *repo_pa
 	repo_write.temp_fds = temp_fds;
 	repo_write.session_fd = -1;
 	repo_write.session_iev.ibuf.fd = -1;
+	repo_write.protected_refs = protected_refs;
+	repo_write.protected_namespaces = protected_namespaces;
 
 	STAILQ_INIT(&repo_write_client.ref_updates);
 
blob - cb5ff4a606c537ef026d2f095e10c280b2ebe87b
blob + a8986215fe7c762d9e6092d4717a394f937d7578
--- gotd/repo_write.h
+++ gotd/repo_write.h
@@ -14,5 +14,6 @@ void repo_write_main(const char *, const char *, int *
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-void repo_write_main(const char *, const char *, int *, int *);
+void repo_write_main(const char *, const char *, int *, int *,
+    struct got_pathlist_head *, struct got_pathlist_head *);
 void repo_write_shutdown(void);
blob - beb02307a41d585965a6d2853eb38b69adeedf87
blob + 2baef036bd213663876faf58ee76e34028149d55
--- gotsh/gotsh.c
+++ gotsh/gotsh.c
@@ -31,6 +31,7 @@
 
 #include "got_error.h"
 #include "got_serve.h"
+#include "got_path.h"
 
 #include "gotd.h"
 
blob - 9722f2b79685f6dcd81c590ca357729edac35a25
blob + c2f4f1a9967a94dd15840bfbd9aae72dd498332a
--- lib/error.c
+++ lib/error.c
@@ -227,7 +227,7 @@ static const struct got_error got_errors[] = {
 	{ GOT_ERR_CLIENT_ID, "unknown client identifier" },
 	{ GOT_ERR_REPO_TEMPFILE, "no repository tempfile available" },
 	{ GOT_ERR_REFS_PROTECTED, "reference namespace may not be modified" },
-	{ GOT_ERR_REF_PROTECTED," reference may not be modified" },
+	{ GOT_ERR_REF_PROTECTED, "reference may not be modified" },
 	{ GOT_ERR_REF_BUSY, "reference cannot be updated; please try again" },
 	{ GOT_ERR_COMMIT_BAD_AUTHOR, "commit author formatting would "
 	    "make Git unhappy" },
blob - 43f08a42a8c29c7ff51d53a6167ba9fdd8ff9e37
blob + ebb50439f7901807fcf2e02d6999a6b3cc4fb0cd
--- regress/gotd/Makefile
+++ regress/gotd/Makefile
@@ -3,7 +3,8 @@ REGRESS_TARGETS=test_repo_read test_repo_read_group \
 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 test_request_bad
+	test_repo_write test_repo_write_empty test_request_bad \
+	test_repo_write_protected
 NOOBJ=Yes
 CLEANFILES=gotd.conf
 
@@ -134,6 +135,19 @@ prepare_test_repo: ensure_root
 	@$(GOTD_TRAP); $(GOTD_START_CMD)
 	@$(GOTD_TRAP); sleep .5
 
+start_gotd_rw_protected: ensure_root
+	@echo 'listen on "$(GOTD_SOCK)"' > $(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 '    protect branch "foo"' >> $(PWD)/gotd.conf
+	@echo '    protect namespace "refs/tags/"' >> $(PWD)/gotd.conf
+	@echo '    protect reference "refs/heads/main"' >> $(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'
@@ -189,6 +203,12 @@ test_repo_write_empty: prepare_test_repo_empty start_g
 		'env $(GOTD_TEST_ENV) sh ./repo_write_empty.sh'
 	@$(GOTD_STOP_CMD) 2>/dev/null
 	@su -m ${GOTD_USER} -c 'env $(GOTD_TEST_ENV) sh ./check_test_repo.sh'
+
+test_repo_write_protected: prepare_test_repo start_gotd_rw_protected
+	@-$(GOTD_TRAP); su ${GOTD_TEST_USER} -c \
+		'env $(GOTD_TEST_ENV) sh ./repo_write_protected.sh'
+	@$(GOTD_STOP_CMD) 2>/dev/null
+	@su -m ${GOTD_USER} -c 'env $(GOTD_TEST_ENV) sh ./check_test_repo.sh'
 	
 test_request_bad: prepare_test_repo_empty start_gotd_ro
 	@-$(GOTD_TRAP); su -m ${GOTD_TEST_USER} -c \
blob - /dev/null
blob + 388de4405e9a94dcdb45d2fcf65e8027655ed7b7 (mode 644)
--- /dev/null
+++ regress/gotd/repo_write_protected.sh
@@ -0,0 +1,174 @@
+#!/bin/sh
+#
+# Copyright (c) 2023 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_create_protected_branch() {
+	local testroot=`test_init create_protected_branch 1`
+
+	got clone -a -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
+
+	(cd $testroot/wt && got branch foo) >/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got branch failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	echo modified alpha > $testroot/wt/alpha
+	(cd $testroot/wt && got commit -m 'edit alpha') >/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got commit failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+	local commit_id=`git_show_branch_head $testroot/repo-clone foo`
+
+	# Creating a new branch should succeed.
+	got send -q -r $testroot/repo-clone -b foo 2> $testroot/stderr
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got send failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	# Verify that the send operation worked fine.
+	got clone -l ${GOTD_TEST_REPO_URL} | grep foo > $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got clone -l failed unexpectedly" >&2
+		test_done "$testroot" "1"
+		return 1
+	fi
+
+	echo "refs/heads/foo: $commit_id" > $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+	fi
+
+	test_done "$testroot" $ret
+}
+
+test_modify_protected_namespace() {
+	local testroot=`test_init modify_protected_namespace`
+
+	got clone -a -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 tag -r $testroot/repo-clone -m "1.0" 1.0 >/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got tag failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	# Creating a new tag should succeed.
+	got send -q -r $testroot/repo-clone -t 1.0 2> $testroot/stderr
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got send failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	got ref -r $testroot/repo-clone -d refs/tags/1.0 > /dev/null
+	got tag -r $testroot/repo-clone -m "another 1.0" 1.0 >/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got tag failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	# Overwriting an existing tag should fail.
+	got send -q -f -r $testroot/repo-clone -t 1.0 2> $testroot/stderr
+	ret=$?
+	if [ $ret == 0 ]; then
+		echo "got send succeeded unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	if ! egrep -q '(gotsh|got-send-pack): refs/tags/: reference namespace may not be modified' \
+		$testroot/stderr; then
+		echo -n "error message unexpected or missing: " >&2
+		cat $testroot/stderr >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	test_done "$testroot" 0
+}
+
+test_delete_protected_ref() {
+	local testroot=`test_init delete_protected_ref`
+
+	got clone -a -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
+
+	if got send -q -r $testroot/repo-clone -d main 2> $testroot/stderr; then
+		echo "got send succeeded unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	if ! egrep -q '(gotsh|got-send-pack): refs/heads/main: reference may not be modified' \
+		$testroot/stderr; then
+		echo -n "error message unexpected or missing: " >&2
+		cat $testroot/stderr >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	test_done "$testroot" 0
+}
+
+
+test_parseargs "$@"
+run_test test_create_protected_branch
+run_test test_modify_protected_namespace
+run_test test_delete_protected_ref