mirror of
https://codeberg.org/git-pages/git-pages.git
synced 2026-05-14 03:01:48 +00:00
Allow using forge authorization with non-wildcard domains.
The new authorization method combines DNS allowlist and existing forge authorization methods: DNS records are used to determine the allowed repository URL, and forge authorization is used to check for push permissions to that URL.
This commit is contained in:
@@ -120,8 +120,9 @@ The authorization flow for content updates (`PUT`, `PATCH`, `DELETE`, `POST` req
|
||||
4. **Wildcard Match (content):** If the method is `POST`, and a `[[wildcard]]` configuration section exists where the suffix of a hostname (compared label-wise) is equal to `[[wildcard]].domain`, and (for `PUT` requests) the body contains a repository URL, and the requested clone URL is a *matching* clone URL, the request is authorized.
|
||||
- **Index repository:** If the request URL is `scheme://<user>.<host>/`, a *matching* clone URL is computed by templating `[[wildcard]].clone-url` with `<user>` and `<project>`, where `<project>` is computed by templating each element of `[[wildcard]].index-repos` with `<user>`, and `[[wildcard]]` is the section where the match occurred.
|
||||
- **Project repository:** If the request URL is `scheme://<user>.<host>/<project>/`, a *matching* clone URL is computed by templating `[[wildcard]].clone-url` with `<user>` and `<project>`, and `[[wildcard]]` is the section where the match occurred.
|
||||
5. **Forge Authorization:** If the method is `PUT` or `PATCH` or `DELETE`, and (unless the method is `DELETE`) the body contains an archive, and a `[[wildcard]]` configuration section exists where the suffix of a hostname (compared label-wise) is equal to `[[wildcard]].domain`, and `[[wildcard]].authorization` is non-empty, and the request includes a `Forge-Authorization:` header, and the header (when forwarded as `Authorization:`) grants push permissions to a repository at the *matching* clone URL (as defined above) as determined by an API call to the forge, the request is authorized. (This enables publishing a site for a private repository.)
|
||||
5. **Default Deny:** Otherwise, the request is not authorized.
|
||||
5. **Forge Authorization (wildcard):** If the method is `PUT` or `PATCH` or `DELETE`, and (unless the method is `DELETE`) the body contains an archive, and a `[[wildcard]]` configuration section exists where the suffix of a hostname (compared label-wise) is equal to `[[wildcard]].domain`, and `[[wildcard]].authorization` is non-empty, and the request includes a `Forge-Authorization:` header, and the header (when forwarded as `Authorization:`) grants push permissions to a repository at the *matching* clone URL (as defined above) as determined by an API call to the forge, the request is authorized.
|
||||
6. **Forge Authorization (DNS allowlist):** If the method is `PUT` or `PATCH` or `DELETE`, and (unless the method is `DELETE`) the body contains an archive, and the request URL is `scheme://<user>.<host>/`, and a TXT record lookup at `_git-pages-repository.<host>` returns a set of well-formed absolute URLs, and the request includes a `Forge-Authorization:` header, and the header (when forwarded as `Authorization:`) grants push permissions to a repository at any of the URLs in the TXT records as determined by an API call to the forge, the request is authorized.
|
||||
7. **Default Deny:** Otherwise, the request is not authorized.
|
||||
|
||||
The authorization flow for metadata retrieval (`GET` requests with site paths starting with `.git-pages/`) in the following order, with the first of multiple applicable rule taking precedence:
|
||||
|
||||
|
||||
116
src/auth.go
116
src/auth.go
@@ -620,7 +620,7 @@ func checkGogsRepositoryPushPermission(baseURL *url.URL, authorization string) e
|
||||
}
|
||||
|
||||
// Gogs, Gitea, and Forgejo all support the same API here.
|
||||
func fetchGogsAuthorizedUser(baseURL *url.URL, authorization string) (*ForgeUser, error) {
|
||||
func fetchGogsAuthorizedUser(baseURL *url.URL, forgeToken string) (*ForgeUser, error) {
|
||||
request, err := http.NewRequest("GET", baseURL.ResolveReference(&url.URL{
|
||||
Path: "/api/v1/user",
|
||||
}).String(), nil)
|
||||
@@ -628,7 +628,7 @@ func fetchGogsAuthorizedUser(baseURL *url.URL, authorization string) (*ForgeUser
|
||||
panic(err) // misconfiguration
|
||||
}
|
||||
request.Header.Set("Accept", "application/json")
|
||||
request.Header.Set("Authorization", authorization)
|
||||
request.Header.Set("Authorization", forgeToken)
|
||||
|
||||
httpClient := http.Client{Timeout: 5 * time.Second}
|
||||
response, err := httpClient.Do(request)
|
||||
@@ -674,9 +674,34 @@ func fetchGogsAuthorizedUser(baseURL *url.URL, authorization string) (*ForgeUser
|
||||
}, nil
|
||||
}
|
||||
|
||||
func authorizeForgeWithToken(r *http.Request) (*Authorization, error) {
|
||||
authorization := r.Header.Get("Forge-Authorization")
|
||||
if authorization == "" {
|
||||
// Check whether a forge token has access to a repository, and if it does, which user it
|
||||
// belongs to. Precondition: `repoURL` is well-formed.
|
||||
func authorizeGogsUser(repoURL string, forgeToken string) (*Authorization, error) {
|
||||
parsedRepoURL, err := url.Parse(repoURL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err = checkGogsRepositoryPushPermission(parsedRepoURL, forgeToken); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authorizedUser, err := fetchGogsAuthorizedUser(parsedRepoURL, forgeToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Authorization{
|
||||
repoURLs: []string{repoURL},
|
||||
forgeUser: authorizedUser,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validates a provided forge token against a repository URL constructed by mapping the host
|
||||
// and project name via the `[[wildcard]]` section of the configuration file.
|
||||
func authorizeForgeWildcard(r *http.Request) (*Authorization, error) {
|
||||
forgeToken := r.Header.Get("Forge-Authorization")
|
||||
if forgeToken == "" {
|
||||
return nil, AuthError{http.StatusUnauthorized, "missing Forge-Authorization header"}
|
||||
}
|
||||
|
||||
@@ -692,42 +717,56 @@ func authorizeForgeWithToken(r *http.Request) (*Authorization, error) {
|
||||
|
||||
var errs []error
|
||||
for _, pattern := range wildcards {
|
||||
if !pattern.Authorization {
|
||||
continue
|
||||
if pattern.Authorization {
|
||||
if userName, found := pattern.Matches(host); found {
|
||||
repoURL, branch := pattern.ApplyTemplate(userName, projectName)
|
||||
auth, err := authorizeGogsUser(repoURL, forgeToken)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
auth.branch = branch
|
||||
return auth, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(errs) == 0 {
|
||||
errs = append(errs, AuthError{http.StatusUnauthorized, "no matching wildcard domain"})
|
||||
}
|
||||
|
||||
if userName, found := pattern.Matches(host); found {
|
||||
repoURL, branch := pattern.ApplyTemplate(userName, projectName)
|
||||
parsedRepoURL, err := url.Parse(repoURL)
|
||||
if err != nil {
|
||||
panic(err) // misconfiguration
|
||||
}
|
||||
errs = append([]error{
|
||||
AuthError{http.StatusUnauthorized, "not authorized by forge (wildcard)"},
|
||||
}, errs...)
|
||||
return nil, joinErrors(errs...)
|
||||
}
|
||||
|
||||
if err = checkGogsRepositoryPushPermission(parsedRepoURL, authorization); err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
// Validates a provided forge token against a repository URL extracted from the DNS allowlist
|
||||
// records of the target domain (`_git-pages-repository.*`).
|
||||
func authorizeForgeDNSAllowlist(r *http.Request) (*Authorization, error) {
|
||||
forgeToken := r.Header.Get("Forge-Authorization")
|
||||
if forgeToken == "" {
|
||||
return nil, AuthError{http.StatusUnauthorized, "missing Forge-Authorization header"}
|
||||
}
|
||||
|
||||
authorizedUser, err := fetchGogsAuthorizedUser(parsedRepoURL, authorization)
|
||||
var errs []error
|
||||
if dnsAuth, err := authorizeDNSAllowlist(r); err != nil {
|
||||
errs = append(errs, err)
|
||||
} else if dnsAuth != nil {
|
||||
// DNS allows uploads from some repositories, but we don't know yet if the forge token
|
||||
// has a push permission to any of these repositories.
|
||||
for _, repoURL := range dnsAuth.repoURLs {
|
||||
auth, err := authorizeGogsUser(repoURL, forgeToken)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
} else {
|
||||
// There is both DNS authorization and forge authorization.
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
return &Authorization{
|
||||
// This will actually be ignored by the callers of AuthorizeUpdateFromArchive and
|
||||
// AuthorizeDeletion, but we return this information as it makes sense to do
|
||||
// contextually here.
|
||||
repoURLs: []string{repoURL},
|
||||
branch: branch,
|
||||
|
||||
forgeUser: authorizedUser,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
errs = append([]error{
|
||||
AuthError{http.StatusUnauthorized, "not authorized by forge"},
|
||||
AuthError{http.StatusUnauthorized, "not authorized by forge (DNS allowlist)"},
|
||||
}, errs...)
|
||||
return nil, joinErrors(errs...)
|
||||
}
|
||||
@@ -756,13 +795,26 @@ func authorizeDNSChallengeOrForgeWithToken(r *http.Request) (*Authorization, err
|
||||
}
|
||||
|
||||
// Token authorization allows updating a site on a wildcard domain from an archive.
|
||||
auth, err = authorizeForgeWithToken(r)
|
||||
// This sub-method uses the `[[wildcard]]` configuration section to derive repository URL.
|
||||
auth, err = authorizeForgeWildcard(r)
|
||||
if err != nil && IsUnauthorized(err) {
|
||||
causes = append(causes, err)
|
||||
} else if err != nil { // bad request
|
||||
return nil, err
|
||||
} else {
|
||||
logc.Printf(r.Context(), "auth: forge token: allow\n")
|
||||
logc.Printf(r.Context(), "auth: forge (wildcard): allow\n")
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
// Token authorization allows updating a site on a wildcard domain from an archive.
|
||||
// This sub-method uses the DNS allowlist authorization mechanism to derive repository URL.
|
||||
auth, err = authorizeForgeDNSAllowlist(r)
|
||||
if err != nil && IsUnauthorized(err) {
|
||||
causes = append(causes, err)
|
||||
} else if err != nil { // bad request
|
||||
return nil, err
|
||||
} else {
|
||||
logc.Printf(r.Context(), "auth: forge (DNS allowlist): allow\n")
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user