Preflight Gitea credentials with repository reads

This commit is contained in:
2026-11-01 17:59:43 +00:00
parent 201af83d95
commit 02bd120aed
3 changed files with 208 additions and 2 deletions

View File

@@ -6,6 +6,7 @@ import (
"io"
"net/http"
"net/url"
"strconv"
"strings"
"go.maximumdirect.net/releasekit/internal/forge"
@@ -33,6 +34,80 @@ type assetHandle struct {
downloadURL string
}
type repositoryResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
FullName string `json:"full_name"`
Owner struct {
Login string `json:"login"`
} `json:"owner"`
}
func (b *Backend) Preflight(ctx context.Context) error {
if strings.TrimSpace(b.destination.Token) == "" {
return &forge.PreflightFailure{Kind: forge.PreflightMissingCredential, Err: errors.New("Gitea token is required")}
}
base, apiRoot, err := b.destinationAPI("preflight repository access")
if err != nil {
return err
}
client := b.client.httpClient()
var resolved repositoryResponse
endpoint := apiRoot
if err := b.client.getJSON(ctx, client, b.destination.Token, "resolve repository", endpoint, &resolved); err != nil {
return preflightFailure(err)
}
if !resolved.matches(b.destination) {
return &forge.PreflightFailure{Kind: forge.PreflightUnavailable, Err: errors.New("repository response does not identify the configured destination")}
}
var authenticated repositoryResponse
apiPrefix := strings.TrimRight(base.String(), "/") + "/api/v1"
endpoint = apiPrefix + "/repositories/" + strconv.FormatInt(resolved.ID, 10)
if err := b.client.getJSON(ctx, client, b.destination.Token, "verify authenticated repository access", endpoint, &authenticated); err != nil {
return preflightFailure(err)
}
if !authenticated.matches(b.destination) || authenticated.ID != resolved.ID {
return &forge.PreflightFailure{Kind: forge.PreflightUnavailable, Err: errors.New("authenticated repository response does not match the resolved destination")}
}
// Gitea's permissions.push describes Git code pushes, not release or asset
// writes. That metadata cannot establish a delivery-permission deficiency.
return nil
}
func (repository repositoryResponse) matches(destination Destination) bool {
if repository.ID <= 0 || !strings.EqualFold(repository.Name, destination.Repository) {
return false
}
if !strings.EqualFold(repository.FullName, destination.Owner+"/"+destination.Repository) {
return false
}
return strings.EqualFold(repository.Owner.Login, destination.Owner)
}
func preflightFailure(err error) error {
var backendFailure *Failure
if !errors.As(err, &backendFailure) {
return &forge.PreflightFailure{Kind: forge.PreflightUnavailable, Err: err}
}
kind := forge.PreflightUnavailable
switch backendFailure.Kind {
case FailureUnauthorized:
if backendFailure.StatusCode == http.StatusUnauthorized {
kind = forge.PreflightInvalidCredential
} else {
kind = forge.PreflightInaccessibleDestination
}
case FailureNotFound:
kind = forge.PreflightInaccessibleDestination
case FailureRejected:
if backendFailure.StatusCode >= http.StatusInternalServerError {
kind = forge.PreflightUnavailable
}
}
return &forge.PreflightFailure{Kind: kind, Err: neutralFailure(err)}
}
func (b *Backend) TagCommit(ctx context.Context, tag string) (string, error) {
if strings.TrimSpace(tag) == "" {
return "", &forge.Failure{Kind: forge.FailureInvalid, Err: errors.New("full tag is required")}