forgejo/routers/api/packages/cargo/cargo.go
merlleu a587d25261
Add auth-required to config.json for Cargo http registry (#26729)
Cargo registry-auth feature requires config.json to have a property
auth-required set to true in order to send token to all registry
requests.
This is ok for git index because you can manually edit the config.json
file to add the auth-required, but when using sparse
(setting index url to
"sparse+https://git.example.com/api/packages/{owner}/cargo/"), the
config.json is dynamically rendered, and does not reflect changes to the
config.json file in the repo.

I see two approaches:
- Serve the real config.json file when fetching the config.json on the
cargo service.
- Automatically detect if the registry requires authorization. (This is
what I implemented in this PR).

What the PR does:
- When a cargo index repository is created, on the config.json, set
auth-required to wether or not the repository is private.
- When the cargo/config.json endpoint is called, set auth-required to
wether or not the request was authorized using an API token.
2023-08-28 07:05:39 +00:00

310 lines
8.2 KiB
Go

// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cargo
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
cargo_module "code.gitea.io/gitea/modules/packages/cargo"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/packages/helper"
"code.gitea.io/gitea/services/convert"
packages_service "code.gitea.io/gitea/services/packages"
cargo_service "code.gitea.io/gitea/services/packages/cargo"
)
// https://doc.rust-lang.org/cargo/reference/registries.html#web-api
type StatusResponse struct {
OK bool `json:"ok"`
Errors []StatusMessage `json:"errors,omitempty"`
}
type StatusMessage struct {
Message string `json:"detail"`
}
func apiError(ctx *context.Context, status int, obj any) {
helper.LogAndProcessError(ctx, status, obj, func(message string) {
ctx.JSON(status, StatusResponse{
OK: false,
Errors: []StatusMessage{
{
Message: message,
},
},
})
})
}
// https://rust-lang.github.io/rfcs/2789-sparse-index.html
func RepositoryConfig(ctx *context.Context) {
ctx.JSON(http.StatusOK, cargo_service.BuildConfig(ctx.Package.Owner, setting.Service.RequireSignInView || ctx.Package.Owner.Visibility != structs.VisibleTypePublic))
}
func EnumeratePackageVersions(ctx *context.Context) {
p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeCargo, ctx.Params("package"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
apiError(ctx, http.StatusNotFound, err)
} else {
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
b, err := cargo_service.BuildPackageIndex(ctx, p)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if b == nil {
apiError(ctx, http.StatusNotFound, nil)
return
}
ctx.PlainTextBytes(http.StatusOK, b.Bytes())
}
type SearchResult struct {
Crates []*SearchResultCrate `json:"crates"`
Meta SearchResultMeta `json:"meta"`
}
type SearchResultCrate struct {
Name string `json:"name"`
LatestVersion string `json:"max_version"`
Description string `json:"description"`
}
type SearchResultMeta struct {
Total int64 `json:"total"`
}
// https://doc.rust-lang.org/cargo/reference/registries.html#search
func SearchPackages(ctx *context.Context) {
page := ctx.FormInt("page")
if page < 1 {
page = 1
}
perPage := ctx.FormInt("per_page")
paginator := db.ListOptions{
Page: page,
PageSize: convert.ToCorrectPageSize(perPage),
}
pvs, total, err := packages_model.SearchLatestVersions(
ctx,
&packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeCargo,
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
IsInternal: util.OptionalBoolFalse,
Paginator: &paginator,
},
)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
crates := make([]*SearchResultCrate, 0, len(pvs))
for _, pd := range pds {
crates = append(crates, &SearchResultCrate{
Name: pd.Package.Name,
LatestVersion: pd.Version.Version,
Description: pd.Metadata.(*cargo_module.Metadata).Description,
})
}
ctx.JSON(http.StatusOK, SearchResult{
Crates: crates,
Meta: SearchResultMeta{
Total: total,
},
})
}
type Owners struct {
Users []OwnerUser `json:"users"`
}
type OwnerUser struct {
ID int64 `json:"id"`
Login string `json:"login"`
Name string `json:"name"`
}
// https://doc.rust-lang.org/cargo/reference/registries.html#owners-list
func ListOwners(ctx *context.Context) {
ctx.JSON(http.StatusOK, Owners{
Users: []OwnerUser{
{
ID: ctx.Package.Owner.ID,
Login: ctx.Package.Owner.Name,
Name: ctx.Package.Owner.DisplayName(),
},
},
})
}
// DownloadPackageFile serves the content of a package
func DownloadPackageFile(ctx *context.Context) {
s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion(
ctx,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeCargo,
Name: ctx.Params("package"),
Version: ctx.Params("version"),
},
&packages_service.PackageFileInfo{
Filename: strings.ToLower(fmt.Sprintf("%s-%s.crate", ctx.Params("package"), ctx.Params("version"))),
},
)
if err != nil {
if err == packages_model.ErrPackageNotExist || err == packages_model.ErrPackageFileNotExist {
apiError(ctx, http.StatusNotFound, err)
return
}
apiError(ctx, http.StatusInternalServerError, err)
return
}
helper.ServePackageFile(ctx, s, u, pf)
}
// https://doc.rust-lang.org/cargo/reference/registries.html#publish
func UploadPackage(ctx *context.Context) {
defer ctx.Req.Body.Close()
cp, err := cargo_module.ParsePackage(ctx.Req.Body)
if err != nil {
apiError(ctx, http.StatusBadRequest, err)
return
}
buf, err := packages_module.CreateHashedBufferFromReader(cp.Content)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer buf.Close()
if buf.Size() != cp.ContentSize {
apiError(ctx, http.StatusBadRequest, "invalid content size")
return
}
pv, _, err := packages_service.CreatePackageAndAddFile(
&packages_service.PackageCreationInfo{
PackageInfo: packages_service.PackageInfo{
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeCargo,
Name: cp.Name,
Version: cp.Version,
},
SemverCompatible: true,
Creator: ctx.Doer,
Metadata: cp.Metadata,
VersionProperties: map[string]string{
cargo_module.PropertyYanked: strconv.FormatBool(false),
},
},
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: strings.ToLower(fmt.Sprintf("%s-%s.crate", cp.Name, cp.Version)),
},
Creator: ctx.Doer,
Data: buf,
IsLead: true,
},
)
if err != nil {
switch err {
case packages_model.ErrDuplicatePackageVersion:
apiError(ctx, http.StatusConflict, err)
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
apiError(ctx, http.StatusForbidden, err)
default:
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
if err := cargo_service.AddOrUpdatePackageIndex(ctx, ctx.Doer, ctx.Package.Owner, pv.PackageID); err != nil {
if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
log.Error("Rollback creation of package version: %v", err)
}
apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.JSON(http.StatusOK, StatusResponse{OK: true})
}
// https://doc.rust-lang.org/cargo/reference/registries.html#yank
func YankPackage(ctx *context.Context) {
yankPackage(ctx, true)
}
// https://doc.rust-lang.org/cargo/reference/registries.html#unyank
func UnyankPackage(ctx *context.Context) {
yankPackage(ctx, false)
}
func yankPackage(ctx *context.Context, yank bool) {
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeCargo, ctx.Params("package"), ctx.Params("version"))
if err != nil {
if err == packages_model.ErrPackageNotExist {
apiError(ctx, http.StatusNotFound, err)
return
}
apiError(ctx, http.StatusInternalServerError, err)
return
}
pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, cargo_module.PropertyYanked)
if err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if len(pps) == 0 {
apiError(ctx, http.StatusInternalServerError, "Property not found")
return
}
pp := pps[0]
pp.Value = strconv.FormatBool(yank)
if err := packages_model.UpdateProperty(ctx, pp); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
if err := cargo_service.AddOrUpdatePackageIndex(ctx, ctx.Doer, ctx.Package.Owner, pv.PackageID); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
ctx.JSON(http.StatusOK, StatusResponse{OK: true})
}