Commit Diff


commit - c343b6811380169034b3341781bac5f3a347e7f6
commit + 1d26dbd4b176e993c088a1b058223d613ef9365c
blob - 8628a270bcaa8929d1e004abd3806a8d7a407f26
blob + 59f265132b4c3bb5c9a65a0446dcdab1fd40b67d
--- gotsysd/gotsysd.c
+++ gotsysd/gotsysd.c
@@ -1529,6 +1529,8 @@ gotsysd_shutdown(void)
 
 	if (gotsysd.db_commit_fd != -1)
 		close(gotsysd.db_commit_fd);
+
+	free(gotsysd.global_repo_access_rules);
 
 	log_info("terminating");
 	exit(0);
@@ -1930,7 +1932,8 @@ main(int argc, char **argv)
 #endif
 		apply_unveil_none();
 
-		sysconf_main(title, gotsysd.uid_start, gotsysd.uid_end);
+		sysconf_main(title, gotsysd.uid_start, gotsysd.uid_end,
+		    gotsysd.global_repo_access_rules);
 		/* NOTREACHED */
 		break;
 	default:
blob - f20dbdab9b039423e97af70ac7b8e753ea7b842d
blob + 9ff79bd55e851c72c79fb7789521958b3eb86210
--- gotsysd/gotsysd.conf.5
+++ gotsysd/gotsysd.conf.5
@@ -91,6 +91,76 @@ user.
 If not specified, the path
 .Pa /git
 will be used.
+.It Ic repository Ic deny Ar identity
+Deny repository access to users with the username
+.Ar identity .
+.Pp
+Access rules set in
+.Nm
+apply to all repositories and override conflicting per-repository access
+rules specified in
+.Xr gotsys.conf 5 .
+.Pp
+Group names may be matched by prepending a colon
+.Pq Sq \&:
+to
+.Ar identity .
+.Pp
+The special user
+.Ar identity
+.Dq *
+(an asterisk) can be used to match all users, including the
+.Dq anonymous
+user.
+.Pp
+Multiple access rules can be specified, and the last matching rule
+determines the action taken.
+If no rule matches, the per-repository rules specified in
+.Xr gotsys.conf 5
+will take effect.
+.It Ic repository Ic permit Ar mode Ar identity
+Permit repository access to users with the username
+.Ar identity .
+.Pp
+Access rules set in
+.Nm
+apply to all repositories and override conflicting per-repository access
+rules specified in
+.Xr gotsys.conf 5 .
+.Pp
+The
+.Ar mode
+argument must be set to either
+.Ic ro
+for read-only access,
+or
+.Ic rw
+for read-write access.
+Group names may be matched by prepending a colon
+.Pq Sq \&:
+to
+.Ar identity .
+.Pp
+The special user
+.Ar identity
+.Dq anonymous
+can be used when public read-only access to repositories over SSH is desired.
+The anonymous user has an empty password, cannot use an SSH public key, and
+can only be granted read-only access.
+.Pp
+The special user
+.Ar identity
+.Dq *
+(an asterisk) can be used to match all users, except the
+.Dq anonymous
+user.
+Read-only anonymous access must be enabled explicitly.
+.Pp
+Multiple access rules can be specified, and the last matching rule
+determines the action taken.
+If no rule matches, the per-repository rules specified in
+.Xr gotsys.conf 5
+will take effect.
 .It Ic uid range Ar start Ar end
 Set the start and end (inclusive) of the range from which
 .Xr gotsysd 8
@@ -129,6 +199,29 @@ listen on "/var/run/gotsysd.sock"
 repository directory "/git"
 uid range 5000 5999
 .Ed
+.Pp
+Regardless of what
+.Xr gotsys.conf 5
+says, allow the user account
+.Dq backup-user
+to read any repository:
+.Bd -literal -offset indent
+repository permit ro backup-user
+.Ed
+.Pp
+Regardless of what
+.Xr gotsys.conf 5
+says, make all repositories read-only:
+.Bd -literal -offset indent
+repository permit ro "*"
+.Ed
+.Pp
+Regardless of what
+.Xr gotsys.conf 5
+says, make all repositories inaccessible:
+.Bd -literal -offset indent
+repository deny "*"
+.Ed
 .Sh SEE ALSO
 .Xr got 1 ,
 .Xr gotd 8 ,
blob - f72e5f0ff065e99ca9f7e168dbd69ae18ba02940
blob + b6a2d93351ce9620926282a1ad82b34e21529cc6
--- gotsysd/gotsysd.h
+++ gotsysd/gotsysd.h
@@ -112,6 +112,7 @@ struct gotsysd {
 	int sysconf_fd;
 	char *sysconf_commit_id_str;
 	struct gotsysd_access_rule_list access_rules;
+	struct gotsys_access_rule_list *global_repo_access_rules;
 	struct event sysconf_tmo;
 	struct gotsysd_pending_sysconf_cmd_list sysconf_pending;
 
@@ -178,6 +179,8 @@ enum gotsysd_imsg_type {
 	GOTSYSD_IMSG_SYSCONF_AUTHORIZED_KEYS_DONE,
 	GOTSYSD_IMSG_SYSCONF_REPO,
 	GOTSYSD_IMSG_SYSCONF_REPOS_DONE,
+	GOTSYSD_IMSG_SYSCONF_GLOBAL_ACCESS_RULE,
+	GOTSYSD_IMSG_SYSCONF_GLOBAL_ACCESS_RULES_DONE,
 	GOTSYSD_IMSG_SYSCONF_ACCESS_RULE,
 	GOTSYSD_IMSG_SYSCONF_ACCESS_RULES_DONE,
 	GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES,
@@ -555,6 +558,8 @@ const struct got_error *gotsys_imsg_recv_authorized_ke
     struct imsg *);
 const struct got_error *gotsys_imsg_recv_authorized_keys(struct imsg *,
     struct gotsys_authorized_keys_list *); 
+const struct got_error *gotsys_imsg_send_access_rule(struct gotsysd_imsgev *,
+    struct gotsys_access_rule *, int);
 const struct got_error *gotsys_imsg_send_repositories(struct gotsysd_imsgev *,
     struct gotsys_repolist *);
 const struct got_error *gotsys_imsg_recv_repository(struct gotsys_repo **,
blob - a9f801243fe0f3232af577fb8cdcfc0cadbb2442
blob + 7e3a2d02aa64a6ad196c47fb3ce747ed6294522b
--- gotsysd/helpers.c
+++ gotsysd/helpers.c
@@ -1015,6 +1015,8 @@ helpers_dispatch_sysconf(int fd, short event, void *ar
 		case GOTSYSD_IMSG_SYSCONF_WRITE_CONF_GROUP_MEMBERS_DONE:
 		case GOTSYSD_IMSG_SYSCONF_WRITE_CONF_GROUPS_DONE:
 		case GOTSYSD_IMSG_SYSCONF_REPO:
+		case GOTSYSD_IMSG_SYSCONF_GLOBAL_ACCESS_RULE:
+		case GOTSYSD_IMSG_SYSCONF_GLOBAL_ACCESS_RULES_DONE:
 		case GOTSYSD_IMSG_SYSCONF_ACCESS_RULE:
 		case GOTSYSD_IMSG_SYSCONF_ACCESS_RULES_DONE:
 		case GOTSYSD_IMSG_SYSCONF_PROTECTED_TAG_NAMESPACES:
blob - be28e070e622a6bb2ad28de4120884dfffc27dfc
blob + 7996125cee248ac4fb8218d775a7a8b6a3f47ddf
--- gotsysd/libexec/gotsys-write-conf/gotsys-write-conf.c
+++ gotsysd/libexec/gotsys-write-conf/gotsys-write-conf.c
@@ -49,6 +49,7 @@ static size_t nprotected_refs_needed;
 static size_t nprotected_refs_received;
 static int gotd_conf_tmpfd = -1;
 static char *gotd_conf_tmppath;
+static struct gotsys_access_rule_list global_repo_access_rules;
 
 enum writeconf_state {
 	WRITECONF_STATE_EXPECT_USERS,
@@ -96,13 +97,88 @@ send_done(struct gotsysd_imsgev *iev)
 }
 
 static const struct got_error *
+write_access_rule(const char *access, const char * authorization,
+    const char *identifier)
+{
+	int ret;
+
+	ret = dprintf(gotd_conf_tmpfd, "\t%s%s%s\n",
+	    access, authorization, identifier);
+	if (ret == -1)
+		return got_error_from_errno2("dprintf", gotd_conf_tmppath);
+	if (ret != 1 + strlen(access) + strlen(authorization) +
+	    strlen(identifier) + 1) {
+		return got_error_fmt(GOT_ERR_IO,
+		    "short write to %s", gotd_conf_tmppath);
+	}
+
+	return NULL;
+}
+
+static const struct got_error *
+write_global_access_rules(void)
+{
+	const struct got_error *err;
+	struct gotsys_access_rule *rule;
+
+	STAILQ_FOREACH(rule, &global_repo_access_rules, entry) {
+		const char *access, *authorization;
+
+		switch (rule->access) {
+		case GOTSYS_ACCESS_DENIED:
+			access = "deny ";
+			break;
+		case GOTSYS_ACCESS_PERMITTED:
+			access = "permit ";
+			break;
+		default:
+			return got_error_fmt(GOT_ERR_PARSE_CONFIG,
+			    "access rule with unknown access flag %d",
+			    rule->access);
+		}
+
+		if (rule->authorization & GOTSYS_AUTH_WRITE)
+			authorization = "rw ";
+		else if (rule->authorization & GOTSYS_AUTH_READ)
+			authorization = "ro ";
+		else
+			authorization = "";
+	
+		if (strcmp(rule->identifier, "*") == 0) {
+			struct gotsys_user *user;
+
+			STAILQ_FOREACH(user, &gotsysconf.users, entry) {
+				/*
+				 * Anonymous read access must be enabled
+				 * explicitly, not via *.
+				 */
+				if (rule->access == GOTSYS_ACCESS_PERMITTED &&
+				    strcmp(user->name, "anonymous") == 0)
+					continue;
+				err = write_access_rule(access, authorization,
+				    user->name);
+				if (err)
+					return err;
+			}
+		} else {
+			err = write_access_rule(access, authorization,
+			    rule->identifier);
+			if (err)
+				return err;
+		}
+	}
+
+	return NULL;
+}
+
+static const struct got_error *
 write_access_rules(struct gotsys_access_rule_list *rules)
 {
+	const struct got_error *err;
 	struct gotsys_access_rule *rule;
 
 	STAILQ_FOREACH(rule, rules, entry) {
 		const char *access, *authorization;
-		int ret;
 
 		switch (rule->access) {
 		case GOTSYS_ACCESS_DENIED:
@@ -124,16 +200,10 @@ write_access_rules(struct gotsys_access_rule_list *rul
 		else
 			authorization = "";
 
-		ret = dprintf(gotd_conf_tmpfd, "\t%s%s%s\n",
-		    access, authorization, rule->identifier);
-		if (ret == -1)
-			return got_error_from_errno2("dprintf",
-			    gotd_conf_tmppath);
-		if (ret != 1 + strlen(access) + strlen(authorization) +
-		    strlen(rule->identifier) + 1) {
-			return got_error_fmt(GOT_ERR_IO,
-			    "short write to %s", gotd_conf_tmppath);
-		}
+		err = write_access_rule(access, authorization,
+		    rule->identifier);
+		if (err)
+			return err;
 	}
 
 	return NULL;
@@ -326,6 +396,10 @@ write_gotd_conf(void)
 		if (err)
 			return err;
 
+		err = write_global_access_rules();
+		if (err)
+			return err;
+
 		err = write_protected_refs(repo);
 		if (err)
 			return err;
@@ -479,8 +553,34 @@ dispatch_event(int fd, short event, void *arg)
 				break;
 			TAILQ_INSERT_TAIL(&gotsysconf.repos, repo, entry);
 			repo_cur = repo;
+			break;
+		}
+		case GOTSYSD_IMSG_SYSCONF_GLOBAL_ACCESS_RULE: {
+			struct gotsys_access_rule *rule;
+			if (writeconf_state != WRITECONF_STATE_EXPECT_REPOS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+			err = gotsys_imsg_recv_access_rule(&rule, &imsg,
+			    NULL, NULL);
+			if (err)
+				break;
+			STAILQ_INSERT_TAIL(&global_repo_access_rules, rule,
+			    entry);
 			break;
 		}
+		case GOTSYSD_IMSG_SYSCONF_GLOBAL_ACCESS_RULES_DONE:
+			if (writeconf_state != WRITECONF_STATE_EXPECT_REPOS) {
+				err = got_error_fmt(GOT_ERR_PRIVSEP_MSG,
+				    "received unexpected imsg %d while in "
+				    "state %d\n", imsg.hdr.type,
+				    writeconf_state);
+				break;
+			}
+			break;
 		case GOTSYSD_IMSG_SYSCONF_ACCESS_RULE: {
 			struct gotsys_access_rule_list *rules;
 			struct gotsys_access_rule *rule;
@@ -647,6 +747,7 @@ main(int argc, char *argv[])
 	while (!attached)
 		sleep(1);
 #endif
+	STAILQ_INIT(&global_repo_access_rules);
 	gotsys_conf_init(&gotsysconf);
 
 	event_init();
blob - 5382a6cf8d17e10cec59c8629b221a1dbf6cf6d2
blob + 845732b3050151e7063b1800afbf3857993212b6
--- gotsysd/parse.y
+++ gotsysd/parse.y
@@ -50,6 +50,7 @@
 
 #include "log.h"
 #include "gotsysd.h"
+#include "gotsys.h"
 
 #ifndef GOTD_USER
 #define GOTD_USER "_gotd"
@@ -95,6 +96,8 @@ static struct gotsysd		*gotsysd;
 static enum gotsysd_procid	gotsysd_proc_id;
 
 static void conf_new_access_rule(enum gotsysd_access, char *);
+static const struct got_error *conf_new_repo_access_rule(enum gotsys_access,
+    int, const char *);
 
 typedef struct {
 	union {
@@ -108,6 +111,7 @@ typedef struct {
 %}
 
 %token	ERROR LISTEN ON USER GOTD DIRECTORY REPOSITORY UID RANGE PERMIT
+%token	DENY RO RW
 
 %token	<v.string>	STRING
 %token	<v.number>	NUMBER
@@ -229,7 +233,43 @@ main		: LISTEN ON STRING {
 				    $2);
 			} else
 				free($2);
+		}
+		| REPOSITORY DENY numberstring {
+			const struct got_error *err;
+
+			err = conf_new_repo_access_rule(GOTSYS_ACCESS_DENIED,
+			    0, $3);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($3);
+				YYERROR;
+			}
+			free($3);
+		}
+		| REPOSITORY PERMIT RO numberstring {
+			const struct got_error *err;
+
+			err = conf_new_repo_access_rule(GOTSYS_ACCESS_PERMITTED,
+			    GOTSYS_AUTH_READ, $4);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($4);
+				YYERROR;
+			}
+			free($4);
 		}
+		| REPOSITORY PERMIT RW numberstring {
+			const struct got_error *err;
+
+			err = conf_new_repo_access_rule(GOTSYS_ACCESS_PERMITTED,
+			    GOTSYS_AUTH_READ | GOTSYS_AUTH_WRITE, $4);
+			if (err) {
+				yyerror("%s", err->msg);
+				free($4);
+				YYERROR;
+			}
+			free($4);
+		}
 		;
 
 %%
@@ -266,6 +306,7 @@ lookup(char *s)
 {
 	/* This has to be sorted always. */
 	static const struct keywords keywords[] = {
+		{ "deny",			DENY },
 		{ "directory",			DIRECTORY },
 		{ "gotd",			GOTD },
 		{ "listen",			LISTEN },
@@ -273,6 +314,8 @@ lookup(char *s)
 		{ "permit",			PERMIT },
 		{ "range",			RANGE },
 		{ "repository",			REPOSITORY },
+		{ "ro",				RO },
+		{ "rw",				RW },
 		{ "uid",			UID },
 		{ "user",			USER },
 	};
@@ -577,6 +620,7 @@ gotsysd_parse_config(const char *filename, enum gotsys
 {
 	struct sym *sym, *next;
 	int require_config_file = 0;
+	struct gotsys_access_rule_list *global_repo_access_rules;
 
 	memset(pgotsysd, 0, sizeof(*pgotsysd));
 
@@ -584,6 +628,16 @@ gotsysd_parse_config(const char *filename, enum gotsys
 	gotsysd_proc_id = proc_id;
 
 	STAILQ_INIT(&gotsysd->access_rules);
+
+	global_repo_access_rules = malloc(sizeof(*global_repo_access_rules));
+	if (global_repo_access_rules == NULL) {
+		fprintf(stderr, "%s: malloc: %s\n", __func__,
+		    strerror(errno));
+		return -1;
+	}
+	gotsysd->global_repo_access_rules = global_repo_access_rules;
+	STAILQ_INIT(gotsysd->global_repo_access_rules);
+	global_repo_access_rules = NULL;
 
 	/* Apply default values. */
 	if (strlcpy(gotsysd->unix_socket_path, GOTSYSD_UNIX_SOCKET,
@@ -740,3 +794,33 @@ conf_new_access_rule(enum gotsysd_access access, char 
 
 	STAILQ_INSERT_TAIL(&gotsysd->access_rules, rule, entry);
 }
+
+static const struct got_error *
+conf_new_repo_access_rule(enum gotsys_access access, int authorization,
+    const char *identifier)
+{
+	const struct got_error *err;
+	struct gotsys_access_rule *rule;
+	const char *name = identifier;
+
+	if (strcmp(name, "*") != 0) {
+		if (name[0] == ':') {
+			name++;
+			err = gotsys_conf_validate_name(name, "group");
+			if (err)
+				return err;
+		} else {
+			err = gotsys_conf_validate_name(name, "user");
+			if (err)
+				return err;
+		}
+	}
+
+	err = gotsys_conf_new_access_rule(&rule, access, authorization,
+	    identifier, NULL, NULL);
+	if (err)
+		return err;
+
+	STAILQ_INSERT_TAIL(gotsysd->global_repo_access_rules, rule, entry);
+	return NULL;
+}
blob - e3f8794ab608fefd10b1564764fb55cb242295fa
blob + 7fee8a40df4446794f8d3b8d9b88ebcfe89559f4
--- gotsysd/sysconf.c
+++ gotsysd/sysconf.c
@@ -79,6 +79,7 @@ static struct gotsysd_sysconf {
 	size_t *nprotected_refs_cur;
 	size_t nprotected_refs_needed;
 	size_t nprotected_refs_received;
+	struct gotsys_access_rule_list *global_repo_access_rules;
 } gotsysd_sysconf;
 
 static struct gotsys_conf gotsysconf;
@@ -729,6 +730,7 @@ static const struct got_error *
 send_gotsysconf(struct gotsysd_imsgev *iev)
 {
 	const struct got_error *err;
+	struct gotsys_access_rule *rule;
 
 	err = gotsys_imsg_send_users(iev, &gotsysconf.users,
 	    GOTSYSD_IMSG_SYSCONF_WRITE_CONF_USERS,
@@ -743,6 +745,18 @@ send_gotsysconf(struct gotsysd_imsgev *iev)
 	    GOTSYSD_IMSG_SYSCONF_WRITE_CONF_GROUPS_DONE);
 	if (err)
 		return err;
+
+	STAILQ_FOREACH(rule, gotsysd_sysconf.global_repo_access_rules, entry) {
+		err = gotsys_imsg_send_access_rule(iev, rule,
+		    GOTSYSD_IMSG_SYSCONF_GLOBAL_ACCESS_RULE);
+		if (err)
+			return err;
+	}
+
+	if (gotsysd_imsg_compose_event(iev,
+	    GOTSYSD_IMSG_SYSCONF_GLOBAL_ACCESS_RULES_DONE, 0, -1,
+	    NULL, 0) == -1)
+		return got_error_from_errno("gotsysd_imsg_compose_event");
 
 	err = gotsys_imsg_send_repositories(iev, &gotsysconf.repos);
 	if (err)
@@ -1234,7 +1248,8 @@ sysconf_dispatch(int fd, short event, void *arg)
 }
 
 void
-sysconf_main(const char *title, uid_t uid_start, uid_t uid_end)
+sysconf_main(const char *title, uid_t uid_start, uid_t uid_end,
+    struct gotsys_access_rule_list *global_repo_access_rules)
 {
 	struct event evsigint, evsigterm, evsighup, evsigusr1;
 	struct gotsysd_imsgev *iev = &gotsysd_sysconf.parent_iev;
@@ -1249,6 +1264,7 @@ sysconf_main(const char *title, uid_t uid_start, uid_t
 	gotsysd_sysconf.state = SYSCONF_STATE_EXPECT_PARSING_SUCCESS;
 	gotsysd_sysconf.uid_start = uid_start;
 	gotsysd_sysconf.uid_end = uid_end;
+	gotsysd_sysconf.global_repo_access_rules = global_repo_access_rules;
 
 	signal_set(&evsigint, SIGINT, sysconf_sighdlr, NULL);
 	signal_set(&evsigterm, SIGTERM, sysconf_sighdlr, NULL);
blob - 69dabe3a4134e1f341dc15b645eb8fbd6f495b93
blob + 8391a0256347fa9315b8a27c887e89719ade65f5
--- gotsysd/sysconf.h
+++ gotsysd/sysconf.h
@@ -14,4 +14,4 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-void sysconf_main(const char *, uid_t, uid_t);
+void sysconf_main(const char *, uid_t, uid_t, struct gotsys_access_rule_list *);
blob - fa11004e9e14a9712ef0b3c3ea8e2c4dfc98123a
blob + 05fad5e984044a6e291365ac1358ec288a6da1c0
--- lib/gotsys_conf.c
+++ lib/gotsys_conf.c
@@ -726,14 +726,16 @@ gotsys_conf_new_access_rule(struct gotsys_access_rule 
 			return got_error_fmt(GOT_ERR_PARSE_CONFIG,
 			    "empty group name in access rule");
 
-		STAILQ_FOREACH(group, groups, entry) {
-			if (strcmp(group->name, name) == 0)
-				break;
-		}
-		if (group == NULL) {
-			return got_error_fmt(GOT_ERR_PARSE_CONFIG,
-			    "reference to undeclared group '%s' via "
-			    "access rule", name);
+		if (groups) {
+			STAILQ_FOREACH(group, groups, entry) {
+				if (strcmp(group->name, name) == 0)
+					break;
+			}
+			if (group == NULL) {
+				return got_error_fmt(GOT_ERR_PARSE_CONFIG,
+				    "reference to undeclared group '%s' via "
+				    "access rule", name);
+			}
 		}
 	} else if (strcmp(name, "anonymous") == 0) {
 		if (access == GOTSYS_ACCESS_PERMITTED &&
@@ -742,7 +744,7 @@ gotsys_conf_new_access_rule(struct gotsys_access_rule 
 			    "the \"anonymous\" user must not have write "
 			    "permission");
 		}
-	} else {
+	} else if (users) {
 		struct gotsys_user *user = NULL;
 
 		STAILQ_FOREACH(user, users, entry) {
blob - 7d5a4aa34f40e6f7de34db562bf6b6c32e22df98
blob + be6c5e0e7bf717619f74605bb70de1188f08480e
--- lib/gotsys_imsg.c
+++ lib/gotsys_imsg.c
@@ -616,9 +616,9 @@ done:
 	return err;
 }
 
-static const struct got_error *
-send_access_rule(struct gotsysd_imsgev *iev,
-    struct gotsys_access_rule *rule)
+const struct got_error *
+gotsys_imsg_send_access_rule(struct gotsysd_imsgev *iev,
+    struct gotsys_access_rule *rule, int imsg_type)
 {
 	struct gotsysd_imsg_sysconf_access_rule irule;
 	struct ibuf *wbuf = NULL;
@@ -637,7 +637,7 @@ send_access_rule(struct gotsysd_imsgev *iev,
 	irule.authorization = rule->authorization;
 	irule.identifier_len = strlen(rule->identifier);
 
-	wbuf = imsg_create(&iev->ibuf, GOTSYSD_IMSG_SYSCONF_ACCESS_RULE,
+	wbuf = imsg_create(&iev->ibuf, imsg_type,
 	    0, 0, sizeof(irule) + irule.identifier_len);
 	if (wbuf == NULL)
 		return got_error_from_errno("imsg_create SYSCONF_ACCESS_RULE");
@@ -776,7 +776,8 @@ send_repo(struct gotsysd_imsgev *iev, struct gotsys_re
 	imsg_close(&iev->ibuf, wbuf);
 
 	STAILQ_FOREACH(rule, &repo->access_rules, entry) {
-		err = send_access_rule(iev, rule);
+		err = gotsys_imsg_send_access_rule(iev, rule,
+		    GOTSYSD_IMSG_SYSCONF_ACCESS_RULE);
 		if (err)
 			return err;
 	}
blob - 7e60f16b3db43adbee999e9d75a456e2c5327ea1
blob + cd4dfe61444049bb6012ccddeb973b174dbe2083
--- regress/gotsysd/test_gotsysd.sh
+++ regress/gotsysd/test_gotsysd.sh
@@ -1522,6 +1522,107 @@ EOF
 	fi
 
 	echo "gotsh: foo: Permission denied" > $testroot/stderr.expected
+	grep '^gotsh:' $testroot/stderr > $testroot/stderr.filtered
+	cmp -s $testroot/stderr.expected $testroot/stderr.filtered
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr.filtered
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
+test_override_access_rules() {
+	local testroot=`test_init override_access_rules 1`
+
+	# Override gotsys.conf access rules which deny access to foo.git.
+	echo "repository permit ro ${GOTSYSD_DEV_USER}" | \
+		ssh -q -i ${GOTSYSD_SSH_KEY} root@${VMIP} \
+		'cat >> /etc/gotsysd.conf'
+
+	# Restart gotsysd (XXX need a better way to do this...)
+	ssh -q -i ${GOTSYSD_SSH_KEY} root@${VMIP} 'pkill -xf /usr/local/sbin/gotsysd'
+	sleep 1
+	ssh -q -i ${GOTSYSD_SSH_KEY} root@${VMIP} '/usr/local/sbin/gotsysd -vvv'
+	sleep 1
+	ssh -q -i ${GOTSYSD_SSH_KEY} root@${VMIP} 'gotsys apply -w' > /dev/null
+
+	# Cloning repository foo should now succeed.
+	got clone -q -i ${GOTSYSD_SSH_KEY} -b foo \
+		${GOTSYSD_DEV_USER}@${VMIP}:foo.git $testroot/foo.git \
+		> $testroot/stdout 2> $testroot/stderr
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got clone failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	# gotsys.git access should now be read-only.
+	got clone -q -i ${GOTSYSD_SSH_KEY} \
+		${GOTSYSD_DEV_USER}@${VMIP}:gotsys.git $testroot/gotsys2.git \
+		> $testroot/stdout 2> $testroot/stderr
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got clone failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+	got checkout -q $testroot/gotsys2.git $testroot/wt >/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got checkout failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	crypted_vm_pw=`echo ${GOTSYSD_VM_PASSWORD} | encrypt | tr -d '\n'`
+	crypted_pw=`echo ${GOTSYSD_DEV_PASSWORD} | encrypt | tr -d '\n'`
+	sshkey=`cat ${GOTSYSD_SSH_PUBKEY}`
+	cat > ${testroot}/wt/gotsys.conf <<EOF
+group slackers
+
+user ${GOTSYSD_TEST_USER} {
+	password "${crypted_vm_pw}" 
+	authorized key ${sshkey}
+}
+user ${GOTSYSD_DEV_USER} {
+	password "${crypted_pw}" 
+	authorized key ${sshkey}
+}
+repository gotsys.git {
+	permit rw ${GOTSYSD_TEST_USER}
+	permit rw ${GOTSYSD_DEV_USER}
+}
+repository "foo" {
+	deny ${GOTSYSD_DEV_USER}
+	permit ro anonymous
+	head foo
+	protect branch foo
+	protect {
+		tag namespace "refs/tags"
+	}
+}
+repository "bar" {
+	permit rw ${GOTSYSD_DEV_USER}
+}
+EOF
+	(cd ${testroot}/wt && \
+		got commit -m "add bar.git repository" >/dev/null)
+	local commit_id=`git_show_head $testroot/gotsys2.git`
+
+	got send -q -i ${GOTSYSD_SSH_KEY} -r ${testroot}/gotsys2.git \
+		> $testroot/stdout 2> $testroot/stderr
+	ret=$?
+	if [ $ret -eq 0 ]; then
+		echo "got send succeeded unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	echo "gotsh: gotsys.git: Permission denied" > $testroot/stderr.expected
 	grep '^gotsh:' $testroot/stderr > $testroot/stderr.filtered
 	cmp -s $testroot/stderr.expected $testroot/stderr.filtered
 	ret=$?
@@ -1530,7 +1631,74 @@ EOF
 		test_done "$testroot" "$ret"
 		return 1
 	fi
+	test_done "$testroot" "$ret"
+}
 
+test_override_all_user_access() {
+	local testroot=`test_init override_all_user_access 1`
+
+	# Override gotsys.conf access rules which deny access to foo.git.
+	echo 'repository deny "*"' | \
+		ssh -q -i ${GOTSYSD_SSH_KEY} root@${VMIP} \
+		'cat >> /etc/gotsysd.conf'
+
+	# Restart gotsysd (XXX need a better way to do this...)
+	ssh -q -i ${GOTSYSD_SSH_KEY} root@${VMIP} 'pkill -xf /usr/local/sbin/gotsysd'
+	sleep 1
+	ssh -q -i ${GOTSYSD_SSH_KEY} root@${VMIP} '/usr/local/sbin/gotsysd -vvv'
+	sleep 1
+	ssh -q -i ${GOTSYSD_SSH_KEY} root@${VMIP} 'gotsys apply -w' > /dev/null
+
+	# Cloning any repository as any user should now fail.
+	for user in ${GOTSYSD_TEST_USER} ${GOTSYSD_DEV_USER} anonymous; do
+		got clone -q -i ${GOTSYSD_SSH_KEY} -b foo \
+			${user}@${VMIP}:foo.git $testroot/foo-${user}.git \
+			> $testroot/stdout 2> $testroot/stderr
+		ret=$?
+		if [ $ret -eq 0 ]; then
+			echo "got clone succeeded unexpectedly" >&2
+			test_done "$testroot" 1
+			return 1
+		fi
+
+		echo "got-fetch-pack: foo: Permission denied" \
+			> $testroot/stderr.expected
+		grep '^got-fetch-pack:' $testroot/stderr \
+			> $testroot/stderr.filtered
+		cmp -s $testroot/stderr.expected $testroot/stderr.filtered
+		ret=$?
+		if [ $ret -ne 0 ]; then
+			diff -u $testroot/stderr.expected \
+				$testroot/stderr.filtered
+			test_done "$testroot" "$ret"
+			return 1
+		fi
+
+		got clone -q -i ${GOTSYSD_SSH_KEY} \
+			${user}@${VMIP}:gotsys.git \
+			$testroot/gotsys-${user}.git \
+			> $testroot/stdout 2> $testroot/stderr
+		ret=$?
+		if [ $ret -eq 0 ]; then
+			echo "got clone succeeded unexpectedly" >&2
+			test_done "$testroot" 1
+			return 1
+		fi
+
+		echo "got-fetch-pack: gotsys.git: Permission denied" \
+			> $testroot/stderr.expected
+		grep '^got-fetch-pack:' $testroot/stderr \
+			> $testroot/stderr.filtered
+		cmp -s $testroot/stderr.expected $testroot/stderr.filtered
+		ret=$?
+		if [ $ret -ne 0 ]; then
+			diff -u $testroot/stderr.expected \
+				$testroot/stderr.filtered
+			test_done "$testroot" "$ret"
+			return 1
+		fi
+	done
+
 	test_done "$testroot" "$ret"
 }
 
@@ -1546,3 +1714,5 @@ run_test test_bad_gotsysconf
 run_test test_set_head
 run_test test_protect_refs
 run_test test_deny_access
+run_test test_override_access_rules
+run_test test_override_all_user_access