commit 25bbf24a4442db753e05bed1d01ecded81615376 from: Stefan Sperling date: Fri Aug 29 15:41:38 2025 UTC implement and document gotwebd.conf access permission syntax 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 STRING %token NUMBER %type boolean %type listen_addr +%type 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); +}