Commit Diff


commit - 6344415e75626d992b2fa13cc510c74d0cec80b5
commit + 25bbf24a4442db753e05bed1d01ecded81615376
blob - 6f7bb8b9d454761d0ff210092f43b109d4c69127
blob + c2cc691b9bb15f564d17e48bfb3b36ff76d9c615
--- gotwebd/config.c
+++ gotwebd/config.c
@@ -55,6 +55,7 @@ config_init(struct gotwebd *env)
 	TAILQ_INIT(&env->servers);
 	TAILQ_INIT(&env->sockets);
 	TAILQ_INIT(&env->addresses);
+	STAILQ_INIT(&env->access_rules);
 
 	for (i = 0; i < PRIV_FDS__MAX; i++)
 		env->priv_fd[i] = -1;
blob - 4fabc5af7e3f1c8300538ca5ea44204867738233
blob + f5f12fac0038ec242e8a128d63ef32e2709c7062
--- gotwebd/gotwebd.conf.5
+++ gotwebd/gotwebd.conf.5
@@ -72,9 +72,11 @@ to
 effectively disables chroot.
 .It Ic disable authentication
 Disable authentication, allowing any browser to view any repository.
+This setting can be overridden on a per-server or per-repository basis.
 .It Ic enable authentication Oo Ic insecure Oc
 Enable authentication, requiring browsers to present a login token cookie
-before read-only repositories access is granted.
+before read-only repository access is granted.
+This setting can be overridden on a per-server or per-repository basis.
 .Pp
 Browsers presenting a valid login token cookie will be mapped to the
 user account which obtained the login token over SSH from the
@@ -82,14 +84,13 @@ user account which obtained the login token over SSH f
 command of
 .Xr gotsh 1 .
 .Pp
-Browsers with a missing or invalid cookie will be mapped to the user
-account which runs
+Unauthenticated browsers will be mapped to the user account which runs
 .Xr httpd 8 .
 This user account can be set with the
 .Ic www user
 directive.
-Permitting this user to read a repository allows authentication to be
-bypassed for this particular repository.
+Attempts to read repositories as this user will be denied unless
+authentication is disabled for the repository.
 .Pp
 Unless the
 .Ic insecure
@@ -179,6 +180,19 @@ Defaults to
 .Pp
 This path must be valid in the web server's URL space since browsers
 will attempt to fetch it.
+.It Ic disable authentication
+Disable authentication for this server, allowing any browser to view any repository.
+This setting can be overridden on a per-repository basis.
+.Pp
+If not specified, the global configuration context determines
+whether authentication is disabled.
+.It Ic enable authentication
+Enable authentication, requiring browsers to present a login token cookie
+before read-only repository access is granted.
+This setting can be overridden on a per-repository basis.
+.Pp
+If not specified, the global configuration context determines
+whether authentication is enabled.
 .It Ic logo_url Ar url
 Set a hyperlink for the logo.
 Defaults to
@@ -204,6 +218,74 @@ The
 .Cm chroot
 directive must be used before the server declaration in order to
 take effect.
+.It Ic repository Ar name Brq ...
+Set options which apply to a particular repository served by this server.
+.Pp
+A repository context is declared with a unique
+.Ar name ,
+followed by repository-specific configuration directives inside curly braces.
+The repository
+.Ar name
+is looked up within the
+.Ar repos_path ,
+where it should exist with or without a
+.Dq .git
+suffix.
+If a repository does not exist then the options set in its context will
+be ignored.
+.Pp
+For each repository, access rules can be configured using the
+.Ic permit
+and
+.Ic deny
+configuration directives.
+Multiple access rules can be specified, and the last matching rule
+determines the action taken.
+If no rule matches and authentication is enabled,
+.Xr gotwebd 8
+will behave as if the repository did not exist to avoid revealing
+the existence of secret repositories to unauthorized users.
+.Pp
+The available repository configuration directives are as follows:
+.Bl -tag -width Ds
+.It Ic deny Ar identity
+Deny repository access to users with the username
+.Ar identity .
+Group names may be matched by prepending a colon
+.Pq Sq \&:
+to
+.Ar identity .
+Numeric IDs are also accepted.
+.Pp
+When a
+.Ic deny
+rule matches an error page will be sent back to the browser, revealing the
+existence of the requested repository.
+.It Ic permit Ar identity
+Permit repository access to users with the username
+.Ar identity .
+Group names may be matched by prepending a colon
+.Pq Sq \&:
+to
+.Ar identity .
+Numeric IDs are also accepted.
+.It Ic disable authentication
+Disable authentication, allowing any browser to view the repository.
+Any access rules configured with
+.Ic permit
+or
+.Ic deny
+directives for this repository will be ignored.
+.Pp
+If not specified, the server context or global context determines
+whether authentication is disabled.
+.It Ic enable authentication
+Enable authentication, requiring browsers to present a login token cookie
+before read-only repository access is granted.
+.Pp
+If not specified, the server context or global context determines
+whether authentication is enabled.
+.El
 .It Ic respect_exportok Ar on | off
 Set whether to display the repository only if it contains the magic
 .Pa git-daemon-export-ok
@@ -279,7 +361,7 @@ Default location for the
 listening socket.
 .El
 .Sh EXAMPLES
-A sample configuration:
+A sample configuration which allows public browsing:
 .Bd -literal -offset indent
 www user "www"   # www username needs quotes since www is a keyword
 
@@ -287,6 +369,7 @@ server "localhost" {
 	site_name	"my public repos"
 	site_owner	"Flan Hacker"
 	site_link	"Flan' Projects"
+	disable authentication
 }
 .Ed
 .Pp
@@ -301,8 +384,53 @@ listen on ::1 port 9000
 server "localhost" {
 	site_name	"my public repos"
 	repos_path	"/var/git"
+	disable authentication
 }
 .Ed
+.Pp
+The following example illustrates the use of directives related to
+authentication:
+.Bd -literal -offset indent
+# 3 scopes: global, per-server, per-repository
+
+disable authentication  # override the default which is 'enable'
+
+# Allow user "admin" to read anything unless overridden with a
+# "deny" rule later.
+permit "admin"
+
+server "public" {
+	# inherit global default, i.e. authentication is disabled
+	repos_path "/var/www/got/public"
+}
+
+server "secure" {
+	enable authentication	# override global default
+
+	permit flan_squee	# grant access to flan_squee
+	permit :developers	# grant access to developers group
+
+	repos_path		"/var/git"
+
+	repository "got" {  # /var/git/got and /var/git/got.git
+		# Grant access to users who have authenticated as
+		# the anonymous user to gotsh(1), which anyone with
+		# an SSH client sbould be able to do.
+		# Dumb web crawlers will remain locked out.
+		permit anonymous
+	}
+
+	repository "public" {
+		# As an exception, allow any web browsers and
+		# web crawlers to view this repository.
+		disable authentication
+	}
+
+	repository "secret" {
+		deny admin # not even the admin can read this
+	}
+}
+.Ed
 .Sh SEE ALSO
 .Xr got 1 ,
 .Xr httpd.conf 5 ,
blob - d93b9521294f410e5d6c81e702a484e4d973280b
blob + 8169c11f1d7782271f727508f61407829f6f307b
--- gotwebd/gotwebd.h
+++ gotwebd/gotwebd.h
@@ -308,7 +308,36 @@ struct address {
 	char			 ifname[IFNAMSIZ];
 };
 TAILQ_HEAD(addresslist, address);
+
+enum gotwebd_auth_config {
+	GOTWEBD_AUTH_DISABLED	= 0xf00000ff,
+	GOTWEBD_AUTH_SECURE	= 0x00808000,
+	GOTWEBD_AUTH_INSECURE	= 0x0f7f7f00
+};
 
+enum gotwebd_access {
+	GOTWEBD_ACCESS_DENIED = -1,
+	GOTWEBD_ACCESS_PERMITTED = 1
+};
+
+struct gotwebd_access_rule {
+	STAILQ_ENTRY(gotwebd_access_rule) entry;
+
+	enum gotwebd_access access;
+	char *identifier;
+};
+STAILQ_HEAD(gotwebd_access_rule_list, gotwebd_access_rule);
+
+struct gotwebd_repo {
+	TAILQ_ENTRY(gotwebd_repo)	 entry;
+
+	char name[NAME_MAX];
+
+	enum gotwebd_auth_config	auth_config;
+	struct gotwebd_access_rule_list access_rules;
+};
+TAILQ_HEAD(gotwebd_repolist, gotwebd_repo);
+
 struct server {
 	TAILQ_ENTRY(server)	 entry;
 
@@ -333,6 +362,12 @@ struct server {
 	int		 show_repo_description;
 	int		 show_repo_cloneurl;
 	int		 respect_exportok;
+
+	enum gotwebd_auth_config auth_config;
+	struct gotwebd_access_rule_list access_rules;
+
+	struct gotwebd_repolist	 repos;
+	int			 nrepos;
 };
 TAILQ_HEAD(serverlist, server);
 
@@ -362,12 +397,6 @@ struct socket {
 };
 TAILQ_HEAD(socketlist, socket);
 
-enum gotwebd_auth_config {
-	GOTWEBD_AUTH_DISABLED	= 0xf00000ff,
-	GOTWEBD_AUTH_SECURE	= 0x00808000,
-	GOTWEBD_AUTH_INSECURE	= 0x0f7f7f00
-};
-
 struct passwd;
 struct gotwebd {
 	struct serverlist	servers;
@@ -378,6 +407,7 @@ struct gotwebd {
 	struct event	 auth_pause_ev;
 
 	enum gotwebd_auth_config auth_config;
+	struct gotwebd_access_rule_list access_rules;
 
 	int		 pack_fds[GOTWEB_PACK_NUM_TEMPFILES];
 	int		 priv_fd[PRIV_FDS__MAX];
@@ -544,6 +574,7 @@ int	gotweb_render_unauthorized(struct template *);
 int	gotweb_render_authorized(struct template *);
 
 /* parse.y */
+struct gotwebd_repo * gotwebd_new_repo(const char *);
 int parse_config(const char *, struct gotwebd *);
 int cmdline_symset(char *);
 
blob - a1cc8141bf5de34f6aa065a7e4ec8343012ff868
blob + 9d3b654730ec2f623f12354a7b55a35241f1ee9e
--- gotwebd/parse.y
+++ gotwebd/parse.y
@@ -100,6 +100,12 @@ static struct address	*get_unix_addr(const char *);
 int		 addr_dup_check(struct addresslist *, struct address *);
 void		 add_addr(struct address *);
 
+static struct gotwebd_repo	*new_repo;
+static struct gotwebd_repo	*conf_new_repo(struct server *, const char *);
+static void			 conf_new_access_rule(
+				    struct gotwebd_access_rule_list *,
+				    enum gotwebd_access, char *);
+
 typedef struct {
 	union {
 		long long	 number;
@@ -116,12 +122,13 @@ typedef struct {
 %token	SHOW_SITE_OWNER SHOW_REPO_CLONEURL PORT PREFORK RESPECT_EXPORTOK
 %token	SERVER CHROOT CUSTOM_CSS SOCKET
 %token	SUMMARY_COMMITS_DISPLAY SUMMARY_TAGS_DISPLAY USER AUTHENTICATION
-%token	ENABLE DISABLE INSECURE
+%token	ENABLE DISABLE INSECURE REPOSITORY PERMIT DENY
 
 %token	<v.string>	STRING
 %token	<v.number>	NUMBER
 %type	<v.number>	boolean
 %type	<v.string>	listen_addr
+%type	<v.string>	numberstring
 
 %%
 
@@ -148,6 +155,15 @@ varset		: STRING '=' STRING	{
 				fatal("cannot store variable");
 			free($1);
 			free($3);
+		}
+		;
+
+numberstring	: STRING
+		| NUMBER {
+			if (asprintf(&$$, "%lld", (long long)$1) == -1) {
+				yyerror("asprintf: %s", strerror(errno));
+				YYERROR;
+			}
 		}
 		;
 
@@ -294,6 +310,14 @@ main		: PREFORK NUMBER {
 			gotwebd->auth_sock = sockets_conf_new_socket(-1, h);
 			free($3);
 		}
+		| PERMIT numberstring {
+			conf_new_access_rule(&gotwebd->access_rules,
+			    GOTWEBD_ACCESS_PERMITTED, $2);
+		}
+		| DENY numberstring {
+			conf_new_access_rule(&gotwebd->access_rules,
+			    GOTWEBD_ACCESS_DENIED, $2);
+		}
 		;
 
 server		: SERVER STRING {
@@ -448,10 +472,106 @@ serveropts1	: REPOS_PATH STRING {
 			}
 			new_srv->summary_tags_display = $2;
 		}
+		| DISABLE AUTHENTICATION {
+			if (new_srv->auth_config != 0) {
+				yyerror("ambiguous authentication "
+				    "setting for server %s",
+				    new_srv->name);
+				YYERROR;
+			}
+			new_srv->auth_config = GOTWEBD_AUTH_DISABLED;
+		}
+		| ENABLE AUTHENTICATION {
+			if (new_srv->auth_config != 0) {
+				yyerror("ambiguous authentication "
+				    "setting for server %s",
+				    new_srv->name);
+				YYERROR;
+			}
+			new_srv->auth_config = GOTWEBD_AUTH_SECURE;
+		}
+		| ENABLE AUTHENTICATION INSECURE {
+			if (new_srv->auth_config != 0) {
+				yyerror("ambiguous authentication "
+				    "setting for server %s",
+				    new_srv->name);
+				YYERROR;
+			}
+			new_srv->auth_config = GOTWEBD_AUTH_INSECURE;
+		}
+		| PERMIT numberstring {
+			conf_new_access_rule(&new_srv->access_rules,
+			    GOTWEBD_ACCESS_PERMITTED, $2);
+		}
+		| DENY numberstring {
+			conf_new_access_rule(&new_srv->access_rules,
+			    GOTWEBD_ACCESS_DENIED, $2);
+		}
+		| repository
 		;
 
 serveropts2	: serveropts2 serveropts1 nl
 		| serveropts1 optnl
+		;
+
+repository	: REPOSITORY STRING {
+			struct gotwebd_repo *repo;
+
+			TAILQ_FOREACH(repo, &new_srv->repos, entry) {
+				if (strcmp(repo->name, $2) == 0) {
+					yyerror("duplicate repository "
+					    "'%s' in server '%s'", $2,
+					    new_srv->name);
+					free($2);
+					YYERROR;
+				}
+			}
+
+			new_repo = conf_new_repo(new_srv, $2);
+			free($2);
+		} '{' optnl repoopts2 '}' {
+		}
+		;
+
+repoopts2	: repoopts2 repoopts1 nl
+		| repoopts1 optnl
+		;
+
+repoopts1	: DISABLE AUTHENTICATION {
+			if (new_repo->auth_config != 0) {
+				yyerror("ambiguous authentication "
+				    "setting for repository %s",
+				    new_repo->name);
+				YYERROR;
+			}
+			new_repo->auth_config = GOTWEBD_AUTH_DISABLED;
+		}
+		| ENABLE AUTHENTICATION {
+			if (new_repo->auth_config != 0) {
+				yyerror("ambiguous authentication "
+				    "setting for repository %s",
+				    new_repo->name);
+				YYERROR;
+			}
+			new_repo->auth_config = GOTWEBD_AUTH_SECURE;
+		}
+		| ENABLE AUTHENTICATION INSECURE {
+			if (new_repo->auth_config != 0) {
+				yyerror("ambiguous authentication "
+				    "setting for repository %s",
+				    new_repo->name);
+				YYERROR;
+			}
+			new_repo->auth_config = GOTWEBD_AUTH_INSECURE;
+		}
+		| PERMIT numberstring {
+			conf_new_access_rule(&new_repo->access_rules,
+			    GOTWEBD_ACCESS_PERMITTED, $2);
+		}
+		| DENY numberstring {
+			conf_new_access_rule(&new_repo->access_rules,
+			    GOTWEBD_ACCESS_DENIED, $2);
+		}
 		;
 
 nl		: '\n' optnl
@@ -498,6 +618,7 @@ lookup(char *s)
 		{ "authentication",		AUTHENTICATION },
 		{ "chroot",			CHROOT },
 		{ "custom_css",			CUSTOM_CSS },
+		{ "deny",			DENY },
 		{ "disable",			DISABLE },
 		{ "enable",			ENABLE },
 		{ "insecure",			INSECURE },
@@ -507,9 +628,11 @@ lookup(char *s)
 		{ "max_commits_display",	MAX_COMMITS_DISPLAY },
 		{ "max_repos_display",		MAX_REPOS_DISPLAY },
 		{ "on",				ON },
+		{ "permit",			PERMIT },
 		{ "port",			PORT },
 		{ "prefork",			PREFORK },
 		{ "repos_path",			REPOS_PATH },
+		{ "repository",			REPOSITORY },
 		{ "respect_exportok",		RESPECT_EXPORTOK },
 		{ "server",			SERVER },
 		{ "show_repo_age",		SHOW_REPO_AGE },
@@ -857,6 +980,8 @@ int
 parse_config(const char *filename, struct gotwebd *env)
 {
 	struct sym *sym, *next;
+	struct server *srv;
+	struct gotwebd_repo *repo;
 
 	if (config_init(env) == -1)
 		fatalx("failed to initialize configuration");
@@ -937,6 +1062,14 @@ parse_config(const char *filename, struct gotwebd *env
 		env->auth_config = GOTWEBD_AUTH_SECURE;
 		break;
 	}
+	TAILQ_FOREACH(srv, &env->servers, entry) {
+		if (srv->auth_config == 0)
+			srv->auth_config = env->auth_config;
+		TAILQ_FOREACH(repo, &srv->repos, entry) {
+			if (repo->auth_config == 0)
+				repo->auth_config = srv->auth_config;
+		}
+	}
 
 	return (0);
 }
@@ -995,6 +1128,9 @@ conf_new_server(const char *name)
 	srv->max_commits_display = D_MAXCOMMITDISP;
 	srv->summary_commits_display = D_MAXSLCOMMDISP;
 	srv->summary_tags_display = D_MAXSLTAGDISP;
+
+	STAILQ_INIT(&srv->access_rules);
+	TAILQ_INIT(&srv->repos);
 
 	TAILQ_INSERT_TAIL(&gotwebd->servers, srv, entry);
 	gotwebd->server_cnt++;
@@ -1191,4 +1327,64 @@ add_addr(struct address *h)
 	}
 
 	free(h);
+}
+
+struct gotwebd_repo *
+gotwebd_new_repo(const char *name)
+{
+	struct gotwebd_repo *repo;
+
+	repo = calloc(1, sizeof(*repo));
+	if (repo == NULL)
+		return NULL;
+
+	STAILQ_INIT(&repo->access_rules);
+
+	if (strlcpy(repo->name, name, sizeof(repo->name)) >=
+	    sizeof(repo->name)) {
+		free(repo);
+		errno = ENOSPC;
+		return NULL;
+	}
+
+	return repo;
 }
+
+static struct gotwebd_repo *
+conf_new_repo(struct server *server, const char *name)
+{
+	struct gotwebd_repo *repo;
+
+	if (name[0] == '\0') {
+		fatalx("syntax error: empty repository name found in %s",
+		    file->name);
+	}
+
+	if (strchr(name, '\n') != NULL)
+		fatalx("repository names must not contain linefeeds: %s", name);
+
+	repo = gotwebd_new_repo(name);
+	if (repo == NULL)
+		fatal("gotwebd_new_repo");
+
+	TAILQ_INSERT_TAIL(&server->repos, repo, entry);
+	server->nrepos++;
+
+	return repo;
+};
+
+static void
+conf_new_access_rule(struct gotwebd_access_rule_list *rules,
+    enum gotwebd_access access, char *identifier)
+{
+	struct gotwebd_access_rule *rule;
+
+	rule = calloc(1, sizeof(*rule));
+	if (rule == NULL)
+		fatal("calloc");
+
+	rule->access = access;
+	rule->identifier = identifier;
+
+	STAILQ_INSERT_TAIL(rules, rule, entry);
+}