commit 9afa3de221045d529287cc3fa75fdc2915aed5c1 from: Stefan Sperling date: Tue Apr 04 18:17:59 2023 UTC add support for protecting references against 'got send -f' to gotd ok op@ commit - 20a2922ac9672923f2aa124d670cde69d69b0cc3 commit + 9afa3de221045d529287cc3fa75fdc2915aed5c1 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 + 886f92ecc38e22a85b9de751ac0be7ba7d14d98b --- 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,13 @@ 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_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 @@ -172,7 +172,72 @@ Group names may be matched by prepending a colon 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 @@ -194,6 +259,9 @@ repository "src" { permit rw flan_hacker permit rw :developers permit ro anonymous + + protect branch "main" + protect tag namespace "refs/tags/" } # This repository can be accessed via @@ -203,6 +271,11 @@ repository "openbsd/ports" { 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 @@ -85,6 +85,9 @@ struct gotd_repo { 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); @@ -448,6 +451,7 @@ struct gotd_imsg_auth { 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 #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 #include "got_error.h" +#include "got_path.h" #include "gotd.h" #include "log.h" blob - b7d7e708dc1650571a862cb8f93f26798d1f1037 blob + 3b6daa1a2a93121b0ee884ddd8cd1ceb5e85fa9b --- 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,14 @@ static int conf_limit_user_connections(const char * 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 { @@ -107,6 +116,7 @@ typedef struct { %token PATH ERROR LISTEN ON USER REPOSITORY PERMIT DENY %token RO RW CONNECTION LIMIT REQUEST TIMEOUT +%token PROTECT NAMESPACE BRANCH TAG %token STRING %token NUMBER @@ -229,6 +239,44 @@ conflags : REQUEST TIMEOUT timeout { } ; +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; @@ -241,7 +289,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 +300,8 @@ repository : REPOSITORY 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 +335,7 @@ repoopts1 : PATH STRING { GOTD_ACCESS_DENIED, 0, $2); } } + | protect ; repoopts2 : repoopts2 repoopts1 nl @@ -332,17 +383,21 @@ 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 }, { "repository", REPOSITORY }, { "request", REQUEST }, { "ro", RO }, { "rw", RW }, + { "tag", TAG }, { "timeout", TIMEOUT }, { "user", USER }, }; @@ -811,6 +866,9 @@ conf_new_repo(const char *name) 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)) @@ -839,6 +897,94 @@ conf_new_access_rule(struct gotd_repo *repo, enum gotd 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) { @@ -911,3 +1057,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 + 317d11a2dde1181dd8dca488ba5ed7f353895c70 --- gotd/repo_write.c +++ gotd/repo_write.c @@ -47,6 +47,8 @@ #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" @@ -69,6 +71,9 @@ static struct repo_write { 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 { @@ -312,7 +317,7 @@ done: } 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); @@ -322,13 +327,279 @@ protect_ref_namespace(struct got_reference *ref, const "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]; @@ -370,6 +641,12 @@ recv_ref_update(struct imsg *imsg) 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; @@ -389,10 +666,10 @@ recv_ref_update(struct imsg *imsg) 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; @@ -1040,7 +1317,9 @@ verify_packfile(void) 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, @@ -1073,17 +1352,51 @@ verify_packfile(void) 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: @@ -1091,10 +1404,51 @@ 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; @@ -1301,6 +1655,9 @@ repo_write_dispatch_session(int fd, short event, void } 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); @@ -1437,7 +1794,10 @@ repo_write_dispatch(int fd, short event, void *arg) 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; @@ -1454,6 +1814,9 @@ 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_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 @@ -14,5 +14,7 @@ * 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 - 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 + 46b6937a40c4a6e26bc8057ab744e6a55b88271c --- lib/error.c +++ lib/error.c @@ -226,8 +226,8 @@ static const struct got_error got_errors[] = { { 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 @@ -23,6 +23,8 @@ struct got_pathlist_head; */ 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 - 0d4231ebe983b9f0f36e32dc6ea4cfa217100d03 blob + babca5450a093226d85ce667e921aa5d399584b8 --- lib/object_open_io.c +++ lib/object_open_io.c @@ -86,7 +86,28 @@ got_object_open_from_packfile(struct got_object **obj, 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 - 29c7c842065b16ecb3aa7fe09de578199bb18ed0 blob + 831119712bcf2cd3955126858b0726abcb140445 --- lib/object_parse.c +++ lib/object_parse.c @@ -110,6 +110,33 @@ got_object_id_hex(struct got_object_id *id, char *buf, 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 - 25889d5c1fc15e6c52fd8f251cc2c9c692d0cbcf blob + 66f9a20dbc56e8dba38401264f0d16410a95e9cf --- lib/pack_index.c +++ lib/pack_index.c @@ -103,33 +103,6 @@ putbe32(char *b, uint32_t n) } 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]; @@ -239,7 +212,7 @@ read_packed_object(struct got_pack *pack, struct got_i 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; @@ -438,7 +411,7 @@ resolve_deltified_object(struct got_pack *pack, struct 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 @@ -3,7 +3,8 @@ 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 @@ start_gotd_rw: 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 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' @@ -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 + 4a5abbaf3b9c5b810ba8185addece65961258f6f (mode 644) --- /dev/null +++ regress/gotd/repo_write_protected.sh @@ -0,0 +1,306 @@ +#!/bin/sh +# +# Copyright (c) 2023 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. + +. ../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