commit - 1e73031b5b7f6d97d2de389636e620fe0de6201f
commit + 6d7eb4f7d125c942358a1f8edf1d350e74141112
blob - 0fba1db7959d1da077ff54a97824b6ec0255df29
blob + 4f337798294f24f408b1ba9738cf112d1f5d5a13
--- gotctl/gotctl.c
+++ gotctl/gotctl.c
#include "got_error.h"
#include "got_version.h"
+#include "got_path.h"
#include "got_lib_gitproto.h"
blob - 04d2d6c654a526a45e5fc7661da7c4d82aff89f1
blob + 879ce9726af53b2e3df4d2233b5fb6db82d2ce52
--- gotd/gotd.c
+++ gotd/gotd.c
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. */
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_tag_namespaces,
+ &repo->protected_branch_namespaces,
+ &repo->protected_branches);
/* NOTREACHED */
exit(0);
default:
blob - 42253ba60317aec22ed6f18dd8917d823ce845ed
blob + 09928aa29395cb1acfaff6303c26cda1adfaf34a
--- gotd/gotd.conf.5
+++ gotd/gotd.conf.5
to
.Ar identity .
Numeric IDs are also accepted.
+.It Ic protect Brq Ar ...
+The
+.Cm protect
+directive may be used to protect branches and tags in a repository
+from being overwritten by potentially destructive client-side commands,
+such as when
+.Cm got send -f
+and
+.Cm git push -f
+are used to change the history of a branch.
+.Pp
+To build a set of protected branches and tags, 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 branch may be created if it does not exist yet.
+Attempts to delete the branch or change its history will be denied.
+.Pp
+If the
+.Ar name
+does not already begin with
+.Dq refs/heads/
+it will be looked up in the
+.Dq refs/heads/
+reference namespace.
+.It Ic branch Ic namespace Ar namespace
+Protect the given reference namespace, assuming that references in
+this namespace represent branches.
+New branches may be created in the namespace.
+Attempts to change the history of branches or delete them will be denied.
+.Pp
+The
+.Ar namespace
+argument must be absolute, starting with
+.Dq refs/ .
+.It Ic tag Ic namespace Ar namespace
+Protect the given reference namespace, assuming that references in
+this namespace represent tags.
+New tags may be created in the namespace.
+Attempts to change or delete existing tags will be denied.
+.Pp
+The
+.Ar namespace
+argument must be absolute, starting with
+.Dq refs/ .
.El
+.Pp
+The special reference 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
.Sh FILES
.Bl -tag -width Ds -compact
.It Pa /etc/gotd.conf
permit rw flan_hacker
permit rw :developers
permit ro anonymous
+
+ protect branch "main"
+ protect tag namespace "refs/tags/"
}
# This repository can be accessed via
permit rw :porters
permit ro anonymous
deny flan_hacker
+
+ protect {
+ branch "main"
+ tag namespace "refs/tags/"
+ }
}
# Use a larger request timeout value:
blob - 1f9b40a97a0e8ed68f190efc0abc407e5fbc5fde
blob + 6453c5edd60a936a27ff6c705694973e711adb82
--- gotd/gotd.h
+++ gotd/gotd.h
char path[PATH_MAX];
struct gotd_access_rule_list rules;
+ struct got_pathlist_head protected_tag_namespaces;
+ struct got_pathlist_head protected_branch_namespaces;
+ struct got_pathlist_head protected_branches;
};
TAILQ_HEAD(gotd_repolist, gotd_repo);
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 - ec712af51ff37e9a9c9342221a919cc6d0565305
blob + 56711725ea5a13b6c1664f42527978986ba7680d
--- gotd/imsg.c
+++ gotd/imsg.c
#include <unistd.h>
#include "got_error.h"
+#include "got_path.h"
#include "got_lib_poll.h"
blob - 2c0352c2f41add38d42370550c49e764389806f5
blob + 4ac000801b8cadce8c293a8ff3878d4ed250d823
--- gotd/listen.c
+++ gotd/listen.c
#include <unistd.h>
#include "got_error.h"
+#include "got_path.h"
#include "gotd.h"
#include "log.h"
blob - 715588536b23a7e19527bba5a45c58c043647d7e
blob + d43ed3e568eed641142fe3b93aec55273e0b2876
--- gotd/parse.y
+++ gotd/parse.y
#include "got_error.h"
#include "got_path.h"
+#include "got_reference.h"
#include "log.h"
#include "gotd.h"
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_namespace(
+ struct got_pathlist_head *, char *);
+static int conf_protect_tag_namespace(struct gotd_repo *,
+ char *);
+static int conf_protect_branch_namespace(
+ struct gotd_repo *, char *);
+static int conf_protect_branch(struct gotd_repo *,
+ char *);
static enum gotd_procid gotd_proc_id;
typedef struct {
%token PATH ERROR LISTEN ON USER REPOSITORY PERMIT DENY
%token RO RW CONNECTION LIMIT REQUEST TIMEOUT
+%token PROTECT NAMESPACE BRANCH TAG
%token <v.string> STRING
%token <v.number> NUMBER
}
;
+protect : PROTECT '{' optnl protectflags_l '}'
+ | PROTECT protectflags
+
+protectflags_l : protectflags optnl protectflags_l
+ | protectflags optnl
+ ;
+
+protectflags : TAG NAMESPACE STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_REPO_WRITE) {
+ if (conf_protect_tag_namespace(new_repo, $3)) {
+ free($3);
+ YYERROR;
+ }
+ }
+ }
+ | BRANCH NAMESPACE STRING {
+ if (gotd_proc_id == PROC_GOTD ||
+ gotd_proc_id == PROC_REPO_WRITE) {
+ if (conf_protect_branch_namespace(new_repo,
+ $3)) {
+ free($3);
+ YYERROR;
+ }
+ free($3);
+ }
+ }
+ | 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;
}
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);
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);
GOTD_ACCESS_DENIED, 0, $2);
}
}
+ | protect
;
repoopts2 : repoopts2 repoopts1 nl
{
/* 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 },
{ "repository", REPOSITORY },
{ "request", REQUEST },
{ "ro", RO },
{ "rw", RW },
+ { "tag", TAG },
{ "timeout", TIMEOUT },
{ "user", USER },
};
fatalx("%s: calloc", __func__);
STAILQ_INIT(&repo->rules);
+ TAILQ_INIT(&repo->protected_tag_namespaces);
+ TAILQ_INIT(&repo->protected_branch_namespaces);
+ TAILQ_INIT(&repo->protected_branches);
if (strlcpy(repo->name, name, sizeof(repo->name)) >=
sizeof(repo->name))
STAILQ_INSERT_TAIL(&repo->rules, rule, entry);
}
+static int
+refname_is_valid(char *refname)
+{
+ if (strlen(refname) < 5 || strncmp(refname, "refs/", 5) != 0) {
+ yyerror("reference name must begin with \"refs/\": %s",
+ refname);
+ return 0;
+ }
+
+ if (!got_ref_name_is_valid(refname)) {
+ yyerror("invalid reference name: %s", refname);
+ return 0;
+ }
+
+ return 1;
+}
+
+static int
+conf_protect_ref_namespace(struct got_pathlist_head *refs, char *namespace)
+{
+ const struct got_error *error;
+ char *s;
+
+ got_path_strip_trailing_slashes(namespace);
+ if (!refname_is_valid(namespace))
+ return -1;
+ if (asprintf(&s, "%s/", namespace) == -1) {
+ yyerror("asprintf: %s", strerror(errno));
+ return -1;
+ }
+
+ error = got_pathlist_insert(NULL, refs, s, NULL);
+ if (error) {
+ yyerror("got_pathlist_insert: %s", error->msg);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+conf_protect_tag_namespace(struct gotd_repo *repo, char *namespace)
+{
+ return conf_protect_ref_namespace(&repo->protected_tag_namespaces,
+ namespace);
+}
+
+static int
+conf_protect_branch_namespace(struct gotd_repo *repo, char *namespace)
+{
+ return conf_protect_ref_namespace(&repo->protected_branch_namespaces,
+ namespace);
+}
+
+static int
+conf_protect_branch(struct gotd_repo *repo, char *branchname)
+{
+ const struct got_error *error;
+ char *refname;
+
+ if (strncmp(branchname, "refs/heads/", 11) != 0) {
+ if (asprintf(&refname, "refs/heads/%s", branchname) == -1) {
+ yyerror("asprintf: %s", strerror(errno));
+ return -1;
+ }
+ } else {
+ refname = strdup(branchname);
+ if (refname == NULL) {
+ yyerror("strdup: %s", strerror(errno));
+ return -1;
+ }
+ }
+
+ if (!refname_is_valid(refname)) {
+ free(refname);
+ return -1;
+ }
+
+ error = got_pathlist_insert(NULL, &repo->protected_branches,
+ refname, NULL);
+ if (error) {
+ yyerror("got_pathlist_insert: %s", error->msg);
+ return -1;
+ }
+
+ return 0;
+}
+
int
symset(const char *nam, const char *val, int persist)
{
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 - 0b3f8f4df38cd7b80fc0ef9f616105f88ad2565a
blob + b44d02618319a11e9c8d2bfe2b43a20797a9522d
--- gotd/repo_imsg.c
+++ gotd/repo_imsg.c
#include "got_error.h"
#include "got_object.h"
+#include "got_path.h"
#include "got_lib_hash.h"
blob - 139661c41ad7cf22c2e398eb71d1739db24202c2
blob + 8f38840876aa9b8a6ecc9eb282b3ac515153d97f
--- gotd/repo_read.c
+++ gotd/repo_read.c
#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 - 7d216c30fd1e06492e24bd23fce03098762ada66
blob + c42b6ea7059df811d07c1a23bc6a6089f931691f
--- gotd/repo_write.c
+++ gotd/repo_write.c
#include "got_lib_hash.h"
#include "got_lib_object.h"
#include "got_lib_object_cache.h"
+#include "got_lib_object_idset.h"
+#include "got_lib_object_parse.h"
#include "got_lib_ratelimit.h"
#include "got_lib_pack.h"
#include "got_lib_pack_index.h"
int *temp_fds;
int session_fd;
struct gotd_imsgev session_iev;
+ struct got_pathlist_head *protected_tag_namespaces;
+ struct got_pathlist_head *protected_branch_namespaces;
+ struct got_pathlist_head *protected_branches;
} repo_write;
struct gotd_ref_update {
}
static const struct got_error *
-protect_ref_namespace(struct got_reference *ref, const char *namespace)
+validate_namespace(const char *namespace)
{
size_t len = strlen(namespace);
"reference namespace '%s'", namespace);
}
- if (strncmp(namespace, got_ref_get_name(ref), len) == 0)
+ return NULL;
+}
+
+static const struct got_error *
+protect_ref_namespace(const char *refname, const char *namespace)
+{
+ const struct got_error *err;
+
+ err = validate_namespace(namespace);
+ if (err)
+ return err;
+
+ if (strncmp(namespace, refname, strlen(namespace)) == 0)
return got_error_fmt(GOT_ERR_REFS_PROTECTED, "%s", namespace);
return NULL;
}
static const struct got_error *
+verify_object_type(struct got_object_id *id, int expected_obj_type,
+ struct got_pack *pack, struct got_packidx *packidx)
+{
+ const struct got_error *err;
+ char hex[SHA1_DIGEST_STRING_LENGTH];
+ struct got_object *obj;
+ int idx;
+ const char *typestr;
+
+ idx = got_packidx_get_object_idx(packidx, id);
+ if (idx == -1) {
+ got_sha1_digest_to_str(id->sha1, hex, sizeof(hex));
+ return got_error_fmt(GOT_ERR_BAD_PACKFILE,
+ "object %s is missing from pack file", hex);
+ }
+
+ err = got_object_open_from_packfile(&obj, id, pack, packidx,
+ idx, repo_write.repo);
+ if (err)
+ return err;
+
+ if (obj->type != expected_obj_type) {
+ got_sha1_digest_to_str(id->sha1, hex, sizeof(hex));
+ got_object_type_label(&typestr, expected_obj_type);
+ err = got_error_fmt(GOT_ERR_OBJ_TYPE,
+ "%s is not pointing at a %s object", hex, typestr);
+ }
+ got_object_close(obj);
+ return err;
+}
+
+static const struct got_error *
+protect_tag_namespace(const char *namespace, struct got_pack *pack,
+ struct got_packidx *packidx, struct gotd_ref_update *ref_update)
+{
+ const struct got_error *err;
+
+ err = validate_namespace(namespace);
+ if (err)
+ return err;
+
+ if (strncmp(namespace, got_ref_get_name(ref_update->ref),
+ strlen(namespace)) != 0)
+ return NULL;
+
+ if (!ref_update->ref_is_new)
+ return got_error_fmt(GOT_ERR_REFS_PROTECTED, "%s", namespace);
+
+ return verify_object_type(&ref_update->new_id, GOT_OBJ_TYPE_TAG,
+ pack, packidx);
+}
+
+static const struct got_error *
+protect_require_yca(struct got_object_id *tip_id,
+ size_t max_commits_to_traverse, struct got_pack *pack,
+ struct got_packidx *packidx, struct got_reference *ref)
+{
+ const struct got_error *err;
+ uint8_t *buf = NULL;
+ size_t len;
+ struct got_object_id *expected_yca_id = NULL;
+ struct got_object *obj = NULL;
+ struct got_commit_object *commit = NULL;
+ char hex[SHA1_DIGEST_STRING_LENGTH];
+ const struct got_object_id_queue *parent_ids;
+ struct got_object_id_queue ids;
+ struct got_object_qid *pid, *qid;
+ struct got_object_idset *traversed_set = NULL;
+ int found_yca = 0, obj_type;
+
+ STAILQ_INIT(&ids);
+
+ err = got_ref_resolve(&expected_yca_id, repo_write.repo, ref);
+ if (err)
+ return err;
+
+ err = got_object_get_type(&obj_type, repo_write.repo, expected_yca_id);
+ if (err)
+ goto done;
+
+ if (obj_type != GOT_OBJ_TYPE_COMMIT) {
+ got_sha1_digest_to_str(expected_yca_id->sha1, hex, sizeof(hex));
+ err = got_error_fmt(GOT_ERR_OBJ_TYPE,
+ "%s is not pointing at a commit object", hex);
+ goto done;
+ }
+
+ traversed_set = got_object_idset_alloc();
+ if (traversed_set == NULL) {
+ err = got_error_from_errno("got_object_idset_alloc");
+ goto done;
+ }
+
+ err = got_object_qid_alloc(&qid, tip_id);
+ if (err)
+ goto done;
+ STAILQ_INSERT_TAIL(&ids, qid, entry);
+ while (!STAILQ_EMPTY(&ids)) {
+ err = check_cancelled(NULL);
+ if (err)
+ break;
+
+ qid = STAILQ_FIRST(&ids);
+ if (got_object_id_cmp(&qid->id, expected_yca_id) == 0) {
+ found_yca = 1;
+ break;
+ }
+
+ if (got_object_idset_num_elements(traversed_set) >=
+ max_commits_to_traverse)
+ break;
+
+ if (got_object_idset_contains(traversed_set, &qid->id)) {
+ STAILQ_REMOVE_HEAD(&ids, entry);
+ got_object_qid_free(qid);
+ qid = NULL;
+ continue;
+ }
+ err = got_object_idset_add(traversed_set, &qid->id, NULL);
+ if (err)
+ goto done;
+
+ err = got_object_open(&obj, repo_write.repo, &qid->id);
+ if (err && err->code != GOT_ERR_NO_OBJ)
+ goto done;
+ err = NULL;
+ if (obj) {
+ err = got_object_commit_open(&commit, repo_write.repo,
+ obj);
+ if (err)
+ goto done;
+ } else {
+ int idx;
+
+ idx = got_packidx_get_object_idx(packidx, &qid->id);
+ if (idx == -1) {
+ got_sha1_digest_to_str(qid->id.sha1,
+ hex, sizeof(hex));
+ err = got_error_fmt(GOT_ERR_BAD_PACKFILE,
+ "object %s is missing from pack file", hex);
+ goto done;
+ }
+
+ err = got_object_open_from_packfile(&obj, &qid->id,
+ pack, packidx, idx, repo_write.repo);
+ if (err)
+ goto done;
+
+ if (obj->type != GOT_OBJ_TYPE_COMMIT) {
+ got_sha1_digest_to_str(qid->id.sha1,
+ hex, sizeof(hex));
+ err = got_error_fmt(GOT_ERR_OBJ_TYPE,
+ "%s is not pointing at a commit object",
+ hex);
+ goto done;
+ }
+
+ err = got_packfile_extract_object_to_mem(&buf, &len,
+ obj, pack);
+ if (err)
+ goto done;
+
+ err = got_object_parse_commit(&commit, buf, len);
+ if (err)
+ goto done;
+
+ free(buf);
+ buf = NULL;
+ }
+
+ got_object_close(obj);
+ obj = NULL;
+
+ STAILQ_REMOVE_HEAD(&ids, entry);
+ got_object_qid_free(qid);
+ qid = NULL;
+
+ if (got_object_commit_get_nparents(commit) == 0)
+ break;
+
+ parent_ids = got_object_commit_get_parent_ids(commit);
+ STAILQ_FOREACH(pid, parent_ids, entry) {
+ err = check_cancelled(NULL);
+ if (err)
+ goto done;
+ err = got_object_qid_alloc(&qid, &pid->id);
+ if (err)
+ goto done;
+ STAILQ_INSERT_TAIL(&ids, qid, entry);
+ qid = NULL;
+ }
+ got_object_commit_close(commit);
+ commit = NULL;
+ }
+
+ if (!found_yca) {
+ err = got_error_fmt(GOT_ERR_REF_PROTECTED, "%s",
+ got_ref_get_name(ref));
+ }
+done:
+ got_object_idset_free(traversed_set);
+ got_object_id_queue_free(&ids);
+ free(buf);
+ if (obj)
+ got_object_close(obj);
+ if (commit)
+ got_object_commit_close(commit);
+ free(expected_yca_id);
+ return err;
+}
+
+static const struct got_error *
+protect_branch_namespace(const char *namespace, struct got_pack *pack,
+ struct got_packidx *packidx, struct gotd_ref_update *ref_update)
+{
+ const struct got_error *err;
+
+ err = validate_namespace(namespace);
+ if (err)
+ return err;
+
+ if (strncmp(namespace, got_ref_get_name(ref_update->ref),
+ strlen(namespace)) != 0)
+ return NULL;
+
+ if (ref_update->ref_is_new) {
+ return verify_object_type(&ref_update->new_id,
+ GOT_OBJ_TYPE_COMMIT, pack, packidx);
+ }
+
+ return protect_require_yca(&ref_update->new_id,
+ be32toh(packidx->hdr.fanout_table[0xff]), pack, packidx,
+ ref_update->ref);
+}
+
+static const struct got_error *
+protect_branch(const char *refname, struct got_pack *pack,
+ struct got_packidx *packidx, struct gotd_ref_update *ref_update)
+{
+ if (strcmp(refname, got_ref_get_name(ref_update->ref)) != 0)
+ return NULL;
+
+ /* Always allow new branches to be created. */
+ if (ref_update->ref_is_new) {
+ return verify_object_type(&ref_update->new_id,
+ GOT_OBJ_TYPE_COMMIT, pack, packidx);
+ }
+
+ return protect_require_yca(&ref_update->new_id,
+ be32toh(packidx->hdr.fanout_table[0xff]), pack, packidx,
+ ref_update->ref);
+}
+
+static const struct got_error *
recv_ref_update(struct imsg *imsg)
{
static const char zero_id[SHA1_DIGEST_LENGTH];
if (err) {
if (err->code != GOT_ERR_NOT_REF)
goto done;
+ if (memcmp(ref_update->new_id.sha1,
+ zero_id, sizeof(zero_id)) == 0) {
+ err = got_error_fmt(GOT_ERR_BAD_OBJ_ID,
+ "%s", refname);
+ goto done;
+ }
err = got_ref_alloc(&ref, refname, &ref_update->new_id);
if (err)
goto done;
goto done;
}
- err = protect_ref_namespace(ref, "refs/got/");
+ err = protect_ref_namespace(got_ref_get_name(ref), "refs/got/");
if (err)
goto done;
- err = protect_ref_namespace(ref, "refs/remotes/");
+ err = protect_ref_namespace(got_ref_get_name(ref), "refs/remotes/");
if (err)
goto done;
struct got_packidx *packidx = NULL;
struct stat sb;
char *id_str = NULL;
- int idx = -1;
+ struct got_object *obj = NULL;
+ struct got_pathlist_entry *pe;
+ char hex[SHA1_DIGEST_STRING_LENGTH];
if (STAILQ_EMPTY(&client->ref_updates)) {
return got_error_msg(GOT_ERR_BAD_REQUEST,
if (ref_update->delete_ref)
continue;
- err = got_object_id_str(&id_str, &ref_update->new_id);
- if (err)
- goto done;
+ TAILQ_FOREACH(pe, repo_write.protected_tag_namespaces, entry) {
+ err = protect_tag_namespace(pe->path, &client->pack,
+ packidx, ref_update);
+ if (err)
+ goto done;
+ }
- idx = got_packidx_get_object_idx(packidx, &ref_update->new_id);
- if (idx == -1) {
- err = got_error_fmt(GOT_ERR_BAD_PACKFILE,
- "advertised object %s is missing from pack file",
- id_str);
+ /*
+ * Objects which already exist in our repository need
+ * not be present in the pack file.
+ */
+ err = got_object_open(&obj, repo_write.repo,
+ &ref_update->new_id);
+ if (err && err->code != GOT_ERR_NO_OBJ)
goto done;
+ err = NULL;
+ if (obj) {
+ got_object_close(obj);
+ obj = NULL;
+ } else {
+ int idx = got_packidx_get_object_idx(packidx,
+ &ref_update->new_id);
+ if (idx == -1) {
+ got_sha1_digest_to_str(ref_update->new_id.sha1,
+ hex, sizeof(hex));
+ err = got_error_fmt(GOT_ERR_BAD_PACKFILE,
+ "object %s is missing from pack file",
+ hex);
+ goto done;
+ }
}
+
+ TAILQ_FOREACH(pe, repo_write.protected_branch_namespaces,
+ entry) {
+ err = protect_branch_namespace(pe->path,
+ &client->pack, packidx, ref_update);
+ if (err)
+ goto done;
+ }
+ TAILQ_FOREACH(pe, repo_write.protected_branches, entry) {
+ err = protect_branch(pe->path, &client->pack,
+ packidx, ref_update);
+ if (err)
+ goto done;
+ }
}
done:
if (close_err && err == NULL)
err = close_err;
free(id_str);
+ if (obj)
+ got_object_close(obj);
return err;
}
static const struct got_error *
+protect_refs_from_deletion(void)
+{
+ const struct got_error *err = NULL;
+ struct repo_write_client *client = &repo_write_client;
+ struct gotd_ref_update *ref_update;
+ struct got_pathlist_entry *pe;
+ const char *refname;
+
+ STAILQ_FOREACH(ref_update, &client->ref_updates, entry) {
+ if (!ref_update->delete_ref)
+ continue;
+
+ refname = got_ref_get_name(ref_update->ref);
+
+ TAILQ_FOREACH(pe, repo_write.protected_tag_namespaces, entry) {
+ err = protect_ref_namespace(refname, pe->path);
+ if (err)
+ return err;
+ }
+
+ TAILQ_FOREACH(pe, repo_write.protected_branch_namespaces,
+ entry) {
+ err = protect_ref_namespace(refname, pe->path);
+ if (err)
+ return err;
+ }
+
+ TAILQ_FOREACH(pe, repo_write.protected_branches, entry) {
+ if (strcmp(refname, pe->path) == 0) {
+ return got_error_fmt(GOT_ERR_REF_PROTECTED,
+ "%s", refname);
+ }
+ }
+ }
+
+ return NULL;
+}
+
+static const struct got_error *
install_packfile(struct gotd_imsgev *iev)
{
struct repo_write_client *client = &repo_write_client;
}
break;
case GOTD_IMSG_RECV_PACKFILE:
+ err = protect_refs_from_deletion();
+ if (err)
+ break;
err = recv_packfile(&have_packfile, &imsg);
if (err) {
log_warnx("receive packfile: %s", err->msg);
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_tag_namespaces,
+ struct got_pathlist_head *protected_branch_namespaces,
+ struct got_pathlist_head *protected_branches)
{
const struct got_error *err = NULL;
struct repo_write_client *client = &repo_write_client;
repo_write.temp_fds = temp_fds;
repo_write.session_fd = -1;
repo_write.session_iev.ibuf.fd = -1;
+ repo_write.protected_tag_namespaces = protected_tag_namespaces;
+ repo_write.protected_branch_namespaces = protected_branch_namespaces;
+ repo_write.protected_branches = protected_branches;
STAILQ_INIT(&repo_write_client.ref_updates);
blob - cb5ff4a606c537ef026d2f095e10c280b2ebe87b
blob + e8192eec3947ce83dcedba9e20048cb0ff7dfc76
--- gotd/repo_write.h
+++ gotd/repo_write.h
* 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 *,
+ struct got_pathlist_head *);
void repo_write_shutdown(void);
blob - 23e06a43a397b5bc76b1d9cf4901e41465d7af8a
blob + afd9bcda117c08da3020fefb04f6b713a6872af9
--- gotsh/gotsh.c
+++ gotsh/gotsh.c
#include "got_error.h"
#include "got_serve.h"
+#include "got_path.h"
#include "gotd.h"
blob - 4129783e86df8d634cf23282925d3ef4658cafaa
blob + f02cebec77dbb2e815b7375242abbcc7c7d970bf
--- lib/error.c
+++ lib/error.c
{ GOT_ERR_BAD_REQUEST, "unexpected request received" },
{ 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_REFS_PROTECTED, "reference namespace is protected" },
+ { GOT_ERR_REF_PROTECTED, "reference is protected" },
{ GOT_ERR_REF_BUSY, "reference cannot be updated; please try again" },
{ GOT_ERR_COMMIT_BAD_AUTHOR, "commit author formatting would "
"make Git unhappy" },
blob - 4b96e21f407d84ac399adbccb5d7e21cf86dad1f
blob + 767c404755f278059ea3f7b3890ee3a3d0188974
--- lib/got_lib_object_parse.h
+++ lib/got_lib_object_parse.h
*/
char *got_object_id_hex(struct got_object_id *, char *, size_t);
+const struct got_error *got_object_type_label(const char **, int);
+
const struct got_error *got_object_qid_alloc_partial(struct got_object_qid **);
struct got_commit_object *got_object_commit_alloc_partial(void);
struct got_tree_entry *got_alloc_tree_entry_partial(void);
blob - 7dd29c4dcce0d135de22b133789e30d3d6f523c7
blob + 413b0529c0a62fa7d9e0e6fdde7a89ac719515dd
--- lib/object_open_io.c
+++ lib/object_open_io.c
struct got_pack *pack, struct got_packidx *packidx, int obj_idx,
struct got_repository *repo)
{
- return got_error(GOT_ERR_NOT_IMPL);
+ const struct got_error *err;
+
+ *obj = got_repo_get_cached_object(repo, id);
+ if (*obj != NULL) {
+ (*obj)->refcnt++;
+ return NULL;
+ }
+
+ err = got_packfile_open_object(obj, pack, packidx, obj_idx, id);
+ if (err)
+ return err;
+ (*obj)->refcnt++;
+
+ err = got_repo_cache_object(repo, id, *obj);
+ if (err) {
+ if (err->code == GOT_ERR_OBJ_EXISTS ||
+ err->code == GOT_ERR_OBJ_TOO_LARGE)
+ err = NULL;
+ return err;
+ }
+ (*obj)->refcnt++;
+ return NULL;
}
const struct got_error *
blob - a902ff1efd4ecf292aa42457d41225adb7c7349f
blob + c0b1855c518c2630ff4a38ada9e593cbd10c74bd
--- lib/object_parse.c
+++ lib/object_parse.c
return got_sha1_digest_to_str(id->sha1, buf, len);
}
+const struct got_error *
+got_object_type_label(const char **label, int obj_type)
+{
+ const struct got_error *err = NULL;
+
+ switch (obj_type) {
+ case GOT_OBJ_TYPE_BLOB:
+ *label = GOT_OBJ_LABEL_BLOB;
+ break;
+ case GOT_OBJ_TYPE_TREE:
+ *label = GOT_OBJ_LABEL_TREE;
+ break;
+ case GOT_OBJ_TYPE_COMMIT:
+ *label = GOT_OBJ_LABEL_COMMIT;
+ break;
+ case GOT_OBJ_TYPE_TAG:
+ *label = GOT_OBJ_LABEL_TAG;
+ break;
+ default:
+ *label = NULL;
+ err = got_error(GOT_ERR_OBJ_TYPE);
+ break;
+ }
+
+ return err;
+}
+
void
got_object_close(struct got_object *obj)
{
blob - 69e5499d5cb289f6d9215e592c5eaa430e891b47
blob + a2fdf06a31ef966e67ff58156ee7fa833ff8e910
--- lib/pack_index.c
+++ lib/pack_index.c
}
static const struct got_error *
-get_obj_type_label(const char **label, int obj_type)
-{
- const struct got_error *err = NULL;
-
- switch (obj_type) {
- case GOT_OBJ_TYPE_BLOB:
- *label = GOT_OBJ_LABEL_BLOB;
- break;
- case GOT_OBJ_TYPE_TREE:
- *label = GOT_OBJ_LABEL_TREE;
- break;
- case GOT_OBJ_TYPE_COMMIT:
- *label = GOT_OBJ_LABEL_COMMIT;
- break;
- case GOT_OBJ_TYPE_TAG:
- *label = GOT_OBJ_LABEL_TAG;
- break;
- default:
- *label = NULL;
- err = got_error(GOT_ERR_OBJ_TYPE);
- break;
- }
-
- return err;
-}
-
-static const struct got_error *
read_checksum(uint32_t *crc, struct got_hash *ctx, int fd, size_t len)
{
uint8_t buf[8192];
if (err)
break;
got_hash_init(&ctx, GOT_HASH_SHA1);
- err = get_obj_type_label(&obj_label, obj->type);
+ err = got_object_type_label(&obj_label, obj->type);
if (err) {
free(data);
break;
err = got_delta_chain_get_base_type(&base_obj_type, &deltas);
if (err)
goto done;
- err = get_obj_type_label(&obj_label, base_obj_type);
+ err = got_object_type_label(&obj_label, base_obj_type);
if (err)
goto done;
if (asprintf(&header, "%s %zd", obj_label, len) == -1) {
blob - 43f08a42a8c29c7ff51d53a6167ba9fdd8ff9e37
blob + 4fef3e998b0e8055a0acac216cc71828bdcea5c5
--- regress/gotd/Makefile
+++ regress/gotd/Makefile
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
@$(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 tag namespace "refs/tags/"' >> $(PWD)/gotd.conf
+ @echo ' protect branch "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'
'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 + 4a5abbaf3b9c5b810ba8185addece65961258f6f (mode 644)
--- /dev/null
+++ regress/gotd/repo_write_protected.sh
+#!/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_tag_namespace() {
+ local testroot=`test_init modify_protected_tag_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 is protected' \
+ $testroot/stderr; then
+ echo -n "error message unexpected or missing: " >&2
+ cat $testroot/stderr >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ # Deleting an existing tag should fail.
+ # 'got send' cannot even do this so we use 'git push'.
+ (cd $testroot/repo-clone && git push -q -d origin refs/tags/1.0 \
+ 2> $testroot/stderr)
+ ret=$?
+ if [ $ret -eq 0 ]; then
+ echo "git push -d succeeded unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ if ! egrep -q '(fatal: remote error|gotsh): refs/tags/: reference namespace is protected' \
+ $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_branch() {
+ local testroot=`test_init delete_protected_branch`
+
+ 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 is protected' \
+ $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_modify_protected_branch() {
+ local testroot=`test_init modify_protected_branch`
+
+ 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 $testroot/repo-clone $testroot/wt >/dev/null
+
+ for i in 1 2 3; do
+ echo "more alpha" >> $testroot/wt/alpha
+ (cd $testroot/wt && got commit -m "more" >/dev/null)
+ done
+ local commit_id=`git_show_head $testroot/repo-clone`
+ local parent_commit_id=`git_show_parent_commit $testroot/repo-clone \
+ "$commit_id"`
+
+ # Modifying the branch by adding new commits on top should succeed.
+ got send -q -r $testroot/repo-clone 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 main > $testroot/stdout
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got clone -l failed unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ echo "HEAD: refs/heads/main" > $testroot/stdout.expected
+ echo "refs/heads/main: $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
+ test_done "$testroot" $ret
+ return 1
+ fi
+
+ # Attempt to remove the tip commit
+ (cd $testroot/wt && got update -c "$parent_commit_id" >/dev/null)
+ (cd $testroot/wt && got histedit -d >/dev/null)
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got histedit failed unexpectedly" >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ # The client should reject sending without -f.
+ 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: refs/heads/main: fetch and rebase required' \
+ >> $testroot/stderr.expected
+ if ! cmp -s $testroot/stderr.expected $testroot/stderr; then
+ diff -u $testroot/stderr.expected $testroot/stderr
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ # Try again with -f.
+ got send -q -r $testroot/repo-clone -f 2> $testroot/stderr
+ ret=$?
+ if [ $ret -eq 0 ]; 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 is protected' \
+ $testroot/stderr; then
+ echo -n "error message unexpected or missing: " >&2
+ cat $testroot/stderr >&2
+ test_done "$testroot" 1
+ return 1
+ fi
+
+ # Verify that the send -f operation did not have any effect.
+ got clone -l ${GOTD_TEST_REPO_URL} | grep main > $testroot/stdout
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "got clone -l failed unexpectedly" >&2
+ test_done "$testroot" "1"
+ return 1
+ fi
+
+ echo "HEAD: refs/heads/main" > $testroot/stdout.expected
+ echo "refs/heads/main: $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_parseargs "$@"
+run_test test_create_protected_branch
+run_test test_modify_protected_tag_namespace
+run_test test_delete_protected_branch
+run_test test_modify_protected_branch