From 9810a346f03c5ca07e84e958e236d41dc9773a10 Mon Sep 17 00:00:00 2001 From: Catherine Date: Fri, 19 Sep 2025 03:25:16 +0000 Subject: [PATCH] Refactor authorization code. NFC --- src/auth.go | 81 +++++++++++++++++++++++++++------------------------- src/pages.go | 24 +++++++++------- 2 files changed, 55 insertions(+), 50 deletions(-) diff --git a/src/auth.go b/src/auth.go index 433ac98..23007cd 100644 --- a/src/auth.go +++ b/src/auth.go @@ -62,7 +62,12 @@ func GetProjectName(r *http.Request) (string, error) { } } -func authorizeDNSChallenge(r *http.Request) ([]string, error) { +type Authorization struct { + // If `nil`, any URL is allowed. If not, only those in the set are allowed. + repoURLs []string +} + +func authorizeDNSChallenge(r *http.Request) (*Authorization, error) { host := GetHost(r) authorization := r.Header.Get("Authorization") @@ -116,10 +121,10 @@ func authorizeDNSChallenge(r *http.Request) ([]string, error) { )} } - return nil, nil + return &Authorization{}, nil } -func authorizeDNSAllowlist(r *http.Request) ([]string, error) { +func authorizeDNSAllowlist(r *http.Request) (*Authorization, error) { host := GetHost(r) allowlistHostname := fmt.Sprintf("_git-pages-repository.%s", host) @@ -139,10 +144,10 @@ func authorizeDNSAllowlist(r *http.Request) ([]string, error) { } } - return repoURLs, err + return &Authorization{repoURLs}, err } -func authorizeWildcardMatch(r *http.Request) ([]string, error) { +func authorizeWildcardMatch(r *http.Request) (*Authorization, error) { host := GetHost(r) hostParts := strings.Split(host, ".") @@ -169,7 +174,7 @@ func authorizeWildcardMatch(r *http.Request) ([]string, error) { "project": projectName, })) } - return repoURLs, nil + return &Authorization{repoURLs}, nil } else { return nil, AuthError{ http.StatusUnauthorized, @@ -181,65 +186,63 @@ func authorizeWildcardMatch(r *http.Request) ([]string, error) { // Returns `repoURLs, err` where if `err == nil` then the request is authorized to clone from // any repository URL included in `repoURLs` (by case-insensitive comparison), or any URL at all // if `repoURLs == nil`. -func authorizeRequest(r *http.Request, allowWildcard bool) ([]string, error) { +func AuthorizeRequest(r *http.Request) (*Authorization, error) { causes := []error{AuthError{http.StatusUnauthorized, "unauthorized"}} if InsecureMode() { log.Println("auth: INSECURE mode: allow *") - return nil, nil // for testing only + return &Authorization{}, nil // for testing only } - repoURLs, err := authorizeDNSChallenge(r) + // DNS challenge gives absolute authority. + auth, err := authorizeDNSChallenge(r) if err != nil && IsUnauthorized(err) { causes = append(causes, err) } else if err != nil { // bad request return nil, err } else { log.Println("auth: DNS challenge: allow *") - return repoURLs, nil + return auth, nil } - repoURLs, err = authorizeDNSAllowlist(r) - if err != nil && IsUnauthorized(err) { - causes = append(causes, err) - } else if err != nil { // bad request - return nil, err - } else { - log.Printf("auth: DNS allowlist: allow %v\n", repoURLs) - return repoURLs, nil - } - - if allowWildcard { - repoURLs, err = authorizeWildcardMatch(r) + // DNS allowlist gives authority to update but not delete. + if r.Method == http.MethodPut || r.Method == http.MethodPut { + auth, err = authorizeDNSAllowlist(r) if err != nil && IsUnauthorized(err) { causes = append(causes, err) } else if err != nil { // bad request return nil, err } else { - log.Printf("auth: wildcard *.%s: allow %v\n", config.Wildcard.Domain, repoURLs) - return repoURLs, nil + log.Printf("auth: DNS allowlist: allow %v\n", auth.repoURLs) + return auth, nil + } + } + + // Wildcard match is only available for webhooks, not the REST API. + if r.Method == http.MethodPost { + auth, err = authorizeWildcardMatch(r) + if err != nil && IsUnauthorized(err) { + causes = append(causes, err) + } else if err != nil { // bad request + return nil, err + } else { + log.Printf("auth: wildcard *.%s: allow %v\n", + config.Wildcard.Domain, auth.repoURLs) + return auth, nil } } return nil, errors.Join(causes...) } -func AuthorizeRequestWithWildcard(r *http.Request) ([]string, error) { - return authorizeRequest(r, true) -} - -func AuthorizeRequestWithoutWildcard(r *http.Request) ([]string, error) { - return authorizeRequest(r, false) -} - -func AuthorizeRepository(repoURL string, allowRepoURLs []string) error { - if allowRepoURLs == nil { +func AuthorizeRepository(repoURL string, auth *Authorization) error { + if auth.repoURLs == nil { return nil // any } allowed := false - for _, allowRepoURL := range allowRepoURLs { - if strings.EqualFold(repoURL, allowRepoURL) { + for _, allowedRepoURL := range auth.repoURLs { + if strings.EqualFold(repoURL, allowedRepoURL) { allowed = true break } @@ -250,7 +253,7 @@ func AuthorizeRepository(repoURL string, allowRepoURLs []string) error { } else { return AuthError{ http.StatusUnauthorized, - fmt.Sprintf("clone URL not in allowlist %v", allowRepoURLs), + fmt.Sprintf("clone URL not in allowlist %v", auth.repoURLs), } } } @@ -259,8 +262,8 @@ func AuthorizeRepository(repoURL string, allowRepoURLs []string) error { // to the site despite the fact that the non-shared-secret authorization methods allow anyone // to impersonate the legitimate webhook sender. (If switching to another repository URL would // be catastrophic, then so would be switching to a different branch.) -func AuthorizeBranch(branch string, allowRepoURLs []string) error { - if allowRepoURLs == nil { +func AuthorizeBranch(branch string, auth *Authorization) error { + if auth.repoURLs == nil { return nil // any } diff --git a/src/pages.go b/src/pages.go index 2e96bed..1defc1b 100644 --- a/src/pages.go +++ b/src/pages.go @@ -155,14 +155,14 @@ func getProjectName(w http.ResponseWriter, r *http.Request) (string, error) { } func putPage(w http.ResponseWriter, r *http.Request) error { - host := GetHost(r) - - projectName, err := GetProjectName(r) + auth, err := AuthorizeRequest(r) if err != nil { return err } - allowedRepoURLs, err := AuthorizeRequestWithoutWildcard(r) + host := GetHost(r) + + projectName, err := GetProjectName(r) if err != nil { return err } @@ -177,7 +177,7 @@ func putPage(w http.ResponseWriter, r *http.Request) error { // request body contains git repository URL repoURL := string(requestBody) - if err := AuthorizeRepository(repoURL, allowedRepoURLs); err != nil { + if err := AuthorizeRepository(repoURL, auth); err != nil { return err } @@ -185,7 +185,7 @@ func putPage(w http.ResponseWriter, r *http.Request) error { if customBranch := r.Header.Get("X-Pages-Branch"); customBranch != "" { branch = customBranch } - if err := AuthorizeBranch(branch, allowedRepoURLs); err != nil { + if err := AuthorizeBranch(branch, auth); err != nil { return err } @@ -219,14 +219,14 @@ func putPage(w http.ResponseWriter, r *http.Request) error { } func postPage(w http.ResponseWriter, r *http.Request) error { - host := GetHost(r) - - projectName, err := GetProjectName(r) + auth, err := AuthorizeRequest(r) if err != nil { return err } - allowedRepoURLs, err := AuthorizeRequestWithWildcard(r) + host := GetHost(r) + + projectName, err := GetProjectName(r) if err != nil { return err } @@ -283,7 +283,7 @@ func postPage(w http.ResponseWriter, r *http.Request) error { webRoot := makeWebRoot(host, projectName) repoURL := event["repository"].(map[string]any)["clone_url"].(string) - if err := AuthorizeRepository(repoURL, allowedRepoURLs); err != nil { + if err := AuthorizeRepository(repoURL, auth); err != nil { return err } @@ -315,10 +315,12 @@ func ServePages(w http.ResponseWriter, r *http.Request) { w.Header().Add("Server", "git-pages") err := error(nil) switch r.Method { + // REST API case http.MethodGet, http.MethodHead: err = getPage(w, r) case http.MethodPut: err = putPage(w, r) + // webhook API case http.MethodPost: err = postPage(w, r) default: