feat: vendor gitea 1.16.2
This commit is contained in:
@@ -54,7 +54,7 @@ func GetRepositoryKey(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{
|
||||
ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{
|
||||
ContentType: "application/x-pem-file",
|
||||
Filename: fmt.Sprintf("%s@%s.rsa.pub", ctx.Package.Owner.LowerName, hex.EncodeToString(fingerprint)),
|
||||
})
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"code.gitea.io/gitea/routers/api/packages/rpm"
|
||||
"code.gitea.io/gitea/routers/api/packages/rubygems"
|
||||
"code.gitea.io/gitea/routers/api/packages/swift"
|
||||
"code.gitea.io/gitea/routers/api/packages/terraform"
|
||||
"code.gitea.io/gitea/routers/api/packages/vagrant"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
@@ -88,21 +89,26 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func verifyAuth(r *web.Router, authMethods []auth.Method) {
|
||||
type verifyAuthOptions struct {
|
||||
afterAuthCallback func(ctx *context.Context, err error)
|
||||
}
|
||||
|
||||
func verifyAuth(r *web.Router, authMethods []auth.Method, opts verifyAuthOptions) {
|
||||
if setting.Service.EnableReverseProxyAuth {
|
||||
authMethods = append(authMethods, &auth.ReverseProxy{})
|
||||
}
|
||||
authGroup := auth.NewGroup(authMethods...)
|
||||
|
||||
r.Use(func(ctx *context.Context) {
|
||||
r.AfterRouting(func(ctx *context.Context) {
|
||||
var err error
|
||||
ctx.Doer, err = authGroup.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
|
||||
if err != nil {
|
||||
ctx.IsSigned = ctx.Doer != nil
|
||||
if opts.afterAuthCallback != nil {
|
||||
opts.afterAuthCallback(ctx, err)
|
||||
} else if err != nil {
|
||||
log.Error("Failed to verify user: %v", err)
|
||||
ctx.HTTPError(http.StatusUnauthorized, "Failed to authenticate user")
|
||||
return
|
||||
}
|
||||
ctx.IsSigned = ctx.Doer != nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -111,15 +117,15 @@ func verifyAuth(r *web.Router, authMethods []auth.Method) {
|
||||
func CommonRoutes() *web.Router {
|
||||
r := web.NewRouter()
|
||||
|
||||
r.Use(context.PackageContexter())
|
||||
r.AfterRouting(context.PackageContexter())
|
||||
|
||||
verifyAuth(r, []auth.Method{
|
||||
&auth.OAuth2{},
|
||||
&auth.Basic{},
|
||||
&nuget.Auth{},
|
||||
&conan.Auth{},
|
||||
&Auth{},
|
||||
&chef.Auth{},
|
||||
})
|
||||
}, verifyAuthOptions{})
|
||||
|
||||
r.Group("/{username}", func() {
|
||||
r.Group("/alpine", func() {
|
||||
@@ -467,9 +473,10 @@ func CommonRoutes() *web.Router {
|
||||
g.MatchPath("HEAD", "/<group:*>/repodata/<filename>", rpm.CheckRepositoryFileExistence)
|
||||
g.MatchPath("GET", "/<group:*>/repodata/<filename>", rpm.GetRepositoryFile)
|
||||
g.MatchPath("PUT", "/<group:*>/upload", reqPackageAccess(perm.AccessModeWrite), rpm.UploadPackageFile)
|
||||
g.MatchPath("POST", "/<group:*>/package/<name>/<version>/errata", reqPackageAccess(perm.AccessModeWrite), rpm.UploadErrata)
|
||||
// this URL pattern is only used internally in the RPM index, it is generated by us, the filename part is not really used (can be anything)
|
||||
g.MatchPath("HEAD,GET", "/<group:*>/package/<name>/<version>/<architecture>", rpm.DownloadPackageFile)
|
||||
g.MatchPath("HEAD,GET", "/<group:*>/package/<name>/<version>/<architecture>/<filename>", rpm.DownloadPackageFile)
|
||||
g.MatchPath("HEAD,GET", "/<group:*>/package/<name>/<version>/<architecture>", rpm.DownloadPackageFile)
|
||||
g.MatchPath("DELETE", "/<group:*>/package/<name>/<version>/<architecture>", reqPackageAccess(perm.AccessModeWrite), rpm.DeletePackageFile)
|
||||
}, reqPackageAccess(perm.AccessModeRead))
|
||||
|
||||
@@ -509,6 +516,21 @@ func CommonRoutes() *web.Router {
|
||||
r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers)
|
||||
}, reqPackageAccess(perm.AccessModeRead))
|
||||
})
|
||||
// See https://docs.gitlab.com/ci/jobs/fine_grained_permissions/#terraform-state-endpoints
|
||||
// For endpoint and permission reference
|
||||
r.Group("/terraform/state/{name}", func() {
|
||||
r.Get("", terraform.GetTerraformState)
|
||||
r.Get("/versions/{serial}", terraform.GetTerraformStateBySerial)
|
||||
r.Group("", func() {
|
||||
r.Post("", terraform.UploadState)
|
||||
r.Delete("", terraform.DeleteState)
|
||||
r.Delete("/versions/{serial}", terraform.DeleteStateBySerial)
|
||||
}, reqPackageAccess(perm.AccessModeWrite))
|
||||
r.Group("/lock", func() {
|
||||
r.Post("", terraform.LockState)
|
||||
r.Delete("", terraform.UnlockState)
|
||||
}, reqPackageAccess(perm.AccessModeWrite))
|
||||
}, reqPackageAccess(perm.AccessModeRead))
|
||||
r.Group("/vagrant", func() {
|
||||
r.Group("/authenticate", func() {
|
||||
r.Get("", vagrant.CheckAuthenticate)
|
||||
@@ -533,11 +555,19 @@ func CommonRoutes() *web.Router {
|
||||
func ContainerRoutes() *web.Router {
|
||||
r := web.NewRouter()
|
||||
|
||||
r.Use(context.PackageContexter())
|
||||
r.AfterRouting(context.PackageContexter())
|
||||
|
||||
verifyAuth(r, []auth.Method{
|
||||
&auth.Basic{},
|
||||
&container.Auth{},
|
||||
// container auth requires token, so container.Authenticate issues a Ghost user token for anonymous access
|
||||
&Auth{AllowGhostUser: true},
|
||||
}, verifyAuthOptions{
|
||||
afterAuthCallback: func(ctx *context.Context, err error) {
|
||||
if err != nil {
|
||||
log.Error("Failed to verify container user: %v", err)
|
||||
container.APIUnauthorizedError(ctx)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// TODO: Content Discovery / References (not implemented yet)
|
||||
|
||||
@@ -35,7 +35,7 @@ func GetRepositoryKey(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{
|
||||
ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{
|
||||
ContentType: "application/pgp-keys",
|
||||
})
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func UploadPackageFile(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
release, err := arch_service.AquireRegistryLock(ctx, ctx.Package.Owner.ID)
|
||||
release, err := arch_service.AcquireRegistryLock(ctx, ctx.Package.Owner.ID)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
@@ -232,7 +232,7 @@ func GetPackageOrRepositoryFile(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ServeContent(bytes.NewReader(data), &context.ServeHeaderOptions{
|
||||
ctx.ServeContent(bytes.NewReader(data), context.ServeHeaderOptions{
|
||||
Filename: filenameOrig,
|
||||
})
|
||||
return
|
||||
@@ -257,7 +257,7 @@ func DeletePackageVersion(ctx *context.Context) {
|
||||
name := ctx.PathParam("name")
|
||||
version := ctx.PathParam("version")
|
||||
|
||||
release, err := arch_service.AquireRegistryLock(ctx, ctx.Package.Owner.ID)
|
||||
release, err := arch_service.AcquireRegistryLock(ctx, ctx.Package.Owner.ID)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
|
||||
+23
-8
@@ -1,7 +1,7 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package conan
|
||||
package packages
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -14,10 +14,13 @@ import (
|
||||
|
||||
var _ auth.Method = &Auth{}
|
||||
|
||||
type Auth struct{}
|
||||
// Auth is for conan and container
|
||||
type Auth struct {
|
||||
AllowGhostUser bool
|
||||
}
|
||||
|
||||
func (a *Auth) Name() string {
|
||||
return "conan"
|
||||
return "packages"
|
||||
}
|
||||
|
||||
// Verify extracts the user from the Bearer token
|
||||
@@ -29,13 +32,25 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS
|
||||
}
|
||||
|
||||
if packageMeta == nil || packageMeta.UserID == 0 {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // the auth method is not applicable
|
||||
}
|
||||
|
||||
u, err := user_model.GetUserByID(req.Context(), packageMeta.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var u *user_model.User
|
||||
switch packageMeta.UserID {
|
||||
case user_model.GhostUserID:
|
||||
if !a.AllowGhostUser {
|
||||
return nil, nil //nolint:nilnil // the auth method is not applicable
|
||||
}
|
||||
u = user_model.NewGhostUser()
|
||||
case user_model.ActionsUserID:
|
||||
u = user_model.NewActionsUserWithTaskID(packageMeta.ActionsUserTaskID)
|
||||
default:
|
||||
u, err = user_model.GetUserByID(req.Context(), packageMeta.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if packageMeta.Scope != "" {
|
||||
store.GetData()["IsApiToken"] = true
|
||||
store.GetData()["ApiTokenScope"] = packageMeta.Scope
|
||||
@@ -61,7 +61,7 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS
|
||||
return nil, err
|
||||
}
|
||||
if u == nil {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // the auth method is not applicable
|
||||
}
|
||||
|
||||
pub, err := getUserPublicKey(req.Context(), u)
|
||||
@@ -88,7 +88,7 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS
|
||||
func getUserFromRequest(req *http.Request) (*user_model.User, error) {
|
||||
username := req.Header.Get("X-Ops-Userid")
|
||||
if username == "" {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // the auth method is not applicable
|
||||
}
|
||||
|
||||
return user_model.GetUserByName(req.Context(), username)
|
||||
|
||||
@@ -9,7 +9,10 @@ import (
|
||||
"time"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
composer_module "code.gitea.io/gitea/modules/packages/composer"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// ServiceIndexResponse contains registry endpoints
|
||||
@@ -91,7 +94,7 @@ type Source struct {
|
||||
Reference string `json:"reference"`
|
||||
}
|
||||
|
||||
func createPackageMetadataResponse(registryURL string, pds []*packages_model.PackageDescriptor) *PackageMetadataResponse {
|
||||
func createPackageMetadataResponse(ctx *context.Context, registryURL string, pds []*packages_model.PackageDescriptor) *PackageMetadataResponse {
|
||||
versions := make([]*PackageVersionMetadata, 0, len(pds))
|
||||
|
||||
for _, pd := range pds {
|
||||
@@ -116,10 +119,15 @@ func createPackageMetadataResponse(registryURL string, pds []*packages_model.Pac
|
||||
},
|
||||
}
|
||||
if pd.Repository != nil {
|
||||
pkg.Source = Source{
|
||||
URL: pd.Repository.HTMLURL(),
|
||||
Type: "git",
|
||||
Reference: pd.Version.Version,
|
||||
permission, err := access_model.GetDoerRepoPermission(ctx, pd.Repository, ctx.Doer)
|
||||
if err != nil {
|
||||
log.Error("GetDoerRepoPermission[%d]: %v", pd.Repository.ID, err)
|
||||
} else if permission.HasAnyUnitAccessOrPublicAccess() {
|
||||
pkg.Source = Source{
|
||||
URL: pd.Repository.HTMLURL(),
|
||||
Type: "git",
|
||||
Reference: pd.Version.Version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,10 @@ package composer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
@@ -23,8 +21,6 @@ import (
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
func apiError(ctx *context.Context, status int, obj any) {
|
||||
@@ -150,6 +146,7 @@ func PackageMetadata(ctx *context.Context) {
|
||||
}
|
||||
|
||||
resp := createPackageMetadataResponse(
|
||||
ctx,
|
||||
setting.AppURL+"api/packages/"+ctx.Package.Owner.Name+"/composer",
|
||||
pds,
|
||||
)
|
||||
@@ -193,7 +190,7 @@ func UploadPackage(ctx *context.Context) {
|
||||
}
|
||||
defer buf.Close()
|
||||
|
||||
cp, err := composer_module.ParsePackage(buf, buf.Size())
|
||||
cp, err := composer_module.ParsePackage(buf, ctx.FormTrim("version"))
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
@@ -209,12 +206,9 @@ func UploadPackage(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if cp.Version == "" {
|
||||
v, err := version.NewVersion(ctx.FormTrim("version"))
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, composer_module.ErrInvalidVersion)
|
||||
return
|
||||
}
|
||||
cp.Version = v.String()
|
||||
// the version should be either set in the "composer.json", or as a query parameter "?version=xxx"
|
||||
apiError(ctx, http.StatusBadRequest, composer_module.ErrInvalidVersion)
|
||||
return
|
||||
}
|
||||
|
||||
_, _, err = packages_service.CreatePackageAndAddFile(
|
||||
@@ -235,7 +229,7 @@ func UploadPackage(ctx *context.Context) {
|
||||
},
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: strings.ToLower(fmt.Sprintf("%s.%s.zip", strings.ReplaceAll(cp.Name, "/", "-"), cp.Version)),
|
||||
Filename: cp.Filename,
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: buf,
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
var _ auth.Method = &Auth{}
|
||||
|
||||
type Auth struct{}
|
||||
|
||||
func (a *Auth) Name() string {
|
||||
return "container"
|
||||
}
|
||||
|
||||
// Verify extracts the user from the Bearer token
|
||||
// If it's an anonymous session, a ghost user is returned
|
||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
|
||||
packageMeta, err := packages.ParseAuthorizationRequest(req)
|
||||
if err != nil {
|
||||
log.Trace("ParseAuthorizationToken: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if packageMeta == nil || packageMeta.UserID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
u, err := user_model.GetPossibleUserByID(req.Context(), packageMeta.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if packageMeta.Scope != "" {
|
||||
store.GetData()["IsApiToken"] = true
|
||||
store.GetData()["ApiTokenScope"] = packageMeta.Scope
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
@@ -26,9 +26,18 @@ import (
|
||||
|
||||
// saveAsPackageBlob creates a package blob from an upload
|
||||
// The uploaded blob gets stored in a special upload version to link them to the package/image
|
||||
func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { //nolint:unparam // PackageBlob is never used
|
||||
// There will be concurrent uploading for the same blob, so it needs a global lock per blob hash
|
||||
func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { //nolint:unparam //returned PackageBlob is never used
|
||||
pb := packages_service.NewPackageBlob(hsr)
|
||||
err := globallock.LockAndDo(ctx, "container-blob:"+pb.HashSHA256, func(ctx context.Context) error {
|
||||
var err error
|
||||
pb, err = saveAsPackageBlobInternal(ctx, hsr, pci, pb)
|
||||
return err
|
||||
})
|
||||
return pb, err
|
||||
}
|
||||
|
||||
func saveAsPackageBlobInternal(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo, pb *packages_model.PackageBlob) (*packages_model.PackageBlob, error) {
|
||||
exists := false
|
||||
|
||||
contentStore := packages_module.NewContentStore()
|
||||
@@ -67,7 +76,7 @@ func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader
|
||||
return createFileForBlob(ctx, uploadVersion, pb)
|
||||
})
|
||||
if err != nil {
|
||||
if !exists {
|
||||
if !exists && pb != nil { // pb can be nil if GetOrInsertBlob failed
|
||||
if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
|
||||
log.Error("Error deleting package blob from content store: %v", err)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ import (
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
container_module "code.gitea.io/gitea/modules/packages/container"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
auth_service "code.gitea.io/gitea/services/auth"
|
||||
@@ -120,17 +122,26 @@ func apiErrorDefined(ctx *context.Context, err *namedError) {
|
||||
})
|
||||
}
|
||||
|
||||
func apiUnauthorizedError(ctx *context.Context) {
|
||||
func APIUnauthorizedError(ctx *context.Context) {
|
||||
// container registry requires that the "/v2" must be in the root, so the sub-path in AppURL should be removed
|
||||
realmURL := httplib.GuessCurrentHostURL(ctx) + "/v2/token"
|
||||
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+realmURL+`",service="container_registry",scope="*"`)
|
||||
|
||||
ownerName := ctx.PathParam("username")
|
||||
owner, _ := user_model.GetUserByName(ctx, ownerName)
|
||||
requireSignIn := owner != nil && owner.Visibility != structs.VisibleTypePublic
|
||||
requireSignIn = requireSignIn || setting.Service.RequireSignInViewStrict
|
||||
if requireSignIn {
|
||||
// support apple container like: container registry login <gitea-host> -u
|
||||
ctx.Resp.Header().Add("WWW-Authenticate", `Basic realm="Gitea Container Registry"`)
|
||||
}
|
||||
apiErrorDefined(ctx, errUnauthorized)
|
||||
}
|
||||
|
||||
// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled)
|
||||
func ReqContainerAccess(ctx *context.Context) {
|
||||
if ctx.Doer == nil || (setting.Service.RequireSignInViewStrict && ctx.Doer.IsGhost()) {
|
||||
apiUnauthorizedError(ctx)
|
||||
APIUnauthorizedError(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +167,7 @@ func Authenticate(ctx *context.Context) {
|
||||
packageScope := auth_service.GetAccessScope(ctx.Data)
|
||||
if u == nil {
|
||||
if setting.Service.RequireSignInViewStrict {
|
||||
apiUnauthorizedError(ctx)
|
||||
APIUnauthorizedError(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -170,7 +181,7 @@ func Authenticate(ctx *context.Context) {
|
||||
if err != nil {
|
||||
log.Error("Error checking access scope: %v", err)
|
||||
}
|
||||
apiUnauthorizedError(ctx)
|
||||
APIUnauthorizedError(ctx)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -704,9 +715,9 @@ func DeleteManifest(ctx *context.Context) {
|
||||
}
|
||||
|
||||
func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) {
|
||||
serveDirectReqParams := make(url.Values)
|
||||
serveDirectReqParams.Set("response-content-type", pfd.Properties.GetByName(container_module.PropertyMediaType))
|
||||
s, u, _, err := packages_service.OpenBlobForDownload(ctx, pfd.File, pfd.Blob, ctx.Req.Method, serveDirectReqParams)
|
||||
s, u, _, err := packages_service.OpenBlobForDownload(ctx, pfd.File, pfd.Blob, ctx.Req.Method, &storage.ServeDirectOptions{
|
||||
ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
|
||||
@@ -35,7 +35,7 @@ func GetRepositoryKey(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{
|
||||
ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{
|
||||
ContentType: "application/pgp-keys",
|
||||
Filename: "repository.key",
|
||||
})
|
||||
@@ -233,7 +233,7 @@ func DownloadPackageFile(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
helper.ServePackageFile(ctx, s, u, pf, &context.ServeHeaderOptions{
|
||||
helper.ServePackageFile(ctx, s, u, pf, context.ServeHeaderOptions{
|
||||
ContentType: "application/vnd.debian.binary-package",
|
||||
Filename: pf.Name,
|
||||
LastModified: pf.CreatedUnix.AsLocalTime(),
|
||||
|
||||
@@ -39,7 +39,7 @@ func ProcessErrorForUser(ctx *context.Context, status int, errObj any) string {
|
||||
|
||||
// ServePackageFile the content of the package file
|
||||
// If the url is set it will redirect the request, otherwise the content is copied to the response.
|
||||
func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf *packages_model.PackageFile, forceOpts ...*context.ServeHeaderOptions) {
|
||||
func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf *packages_model.PackageFile, forceOpts ...context.ServeHeaderOptions) {
|
||||
if u != nil {
|
||||
ctx.Redirect(u.String())
|
||||
return
|
||||
@@ -47,11 +47,11 @@ func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf
|
||||
|
||||
defer s.Close()
|
||||
|
||||
var opts *context.ServeHeaderOptions
|
||||
var opts context.ServeHeaderOptions
|
||||
if len(forceOpts) > 0 {
|
||||
opts = forceOpts[0]
|
||||
} else {
|
||||
opts = &context.ServeHeaderOptions{
|
||||
opts = context.ServeHeaderOptions{
|
||||
Filename: pf.Name,
|
||||
LastModified: pf.CreatedUnix.AsLocalTime(),
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ func servePackageFile(ctx *context.Context, params parameters, serveContent bool
|
||||
return
|
||||
}
|
||||
|
||||
opts := &context.ServeHeaderOptions{
|
||||
opts := context.ServeHeaderOptions{
|
||||
ContentLength: &pb.Size,
|
||||
LastModified: pf.CreatedUnix.AsLocalTime(),
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ func UploadPackage(ctx *context.Context) {
|
||||
canWrite := repo.OwnerID == ctx.Doer.ID
|
||||
|
||||
if !canWrite {
|
||||
perms, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
|
||||
perms, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
|
||||
@@ -6,43 +6,21 @@ package nuget
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
)
|
||||
|
||||
var _ auth.Method = &Auth{}
|
||||
|
||||
type Auth struct{}
|
||||
type Auth struct {
|
||||
basicAuth auth.Basic
|
||||
}
|
||||
|
||||
func (a *Auth) Name() string {
|
||||
return "nuget"
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#request-parameters
|
||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
|
||||
token, err := auth_model.GetAccessTokenBySHA(req.Context(), req.Header.Get("X-NuGet-ApiKey"))
|
||||
if err != nil {
|
||||
if !(auth_model.IsErrAccessTokenNotExist(err) || auth_model.IsErrAccessTokenEmpty(err)) {
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
u, err := user_model.GetUserByID(req.Context(), token.UID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token.UpdatedUnix = timeutil.TimeStampNow()
|
||||
if err := auth_model.UpdateAccessToken(req.Context(), token); err != nil {
|
||||
log.Error("UpdateAccessToken: %v", err)
|
||||
}
|
||||
|
||||
store.GetData()["IsApiToken"] = true
|
||||
store.GetData()["ApiToken"] = token
|
||||
|
||||
return u, nil
|
||||
// ref: https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#request-parameters
|
||||
return a.basicAuth.VerifyAuthToken(req, w, store, sess, req.Header.Get("X-NuGet-ApiKey"))
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
@@ -57,7 +59,7 @@ func GetRepositoryKey(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{
|
||||
ctx.ServeContent(strings.NewReader(pub), context.ServeHeaderOptions{
|
||||
ContentType: "application/pgp-keys",
|
||||
Filename: "repository.key",
|
||||
})
|
||||
@@ -80,7 +82,7 @@ func CheckRepositoryFileExistence(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetServeHeaders(&context.ServeHeaderOptions{
|
||||
ctx.SetServeHeaders(context.ServeHeaderOptions{
|
||||
Filename: pf.Name,
|
||||
LastModified: pf.CreatedUnix.AsLocalTime(),
|
||||
})
|
||||
@@ -219,30 +221,38 @@ func UploadPackageFile(ctx *context.Context) {
|
||||
func DownloadPackageFile(ctx *context.Context) {
|
||||
name := ctx.PathParam("name")
|
||||
version := ctx.PathParam("version")
|
||||
architecture := ctx.PathParam("architecture")
|
||||
group := ctx.PathParam("group")
|
||||
|
||||
s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
|
||||
ctx,
|
||||
&packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeRpm,
|
||||
Name: name,
|
||||
Version: version,
|
||||
},
|
||||
&packages_service.PackageFileInfo{
|
||||
Filename: fmt.Sprintf("%s-%s.%s.rpm", name, version, ctx.PathParam("architecture")),
|
||||
CompositeKey: ctx.PathParam("group"),
|
||||
},
|
||||
ctx.Req.Method,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
openForDownload := func(filename string) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) {
|
||||
return packages_service.OpenFileForDownloadByPackageNameAndVersion(
|
||||
ctx,
|
||||
&packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeRpm,
|
||||
Name: name,
|
||||
Version: version,
|
||||
},
|
||||
&packages_service.PackageFileInfo{
|
||||
Filename: filename,
|
||||
CompositeKey: group,
|
||||
},
|
||||
ctx.Req.Method,
|
||||
)
|
||||
}
|
||||
|
||||
s, u, pf, err := openForDownload(fmt.Sprintf("%s-%s.%s.rpm", name, version, architecture))
|
||||
if errors.Is(err, util.ErrNotExist) && architecture != "noarch" {
|
||||
s, u, pf, err = openForDownload(fmt.Sprintf("%s-%s.%s.rpm", name, version, "noarch"))
|
||||
}
|
||||
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
} else if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
helper.ServePackageFile(ctx, s, u, pf)
|
||||
}
|
||||
|
||||
@@ -316,3 +326,146 @@ func DeletePackageFile(webctx *context.Context) {
|
||||
|
||||
webctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// UploadErrata handles uploading errata information for a package version
|
||||
func UploadErrata(ctx *context.Context) {
|
||||
name := ctx.PathParam("name")
|
||||
version := ctx.PathParam("version")
|
||||
group := ctx.PathParam("group")
|
||||
|
||||
var updates []*rpm_module.Update
|
||||
if err := json.NewDecoder(ctx.Req.Body).Decode(&updates); err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx,
|
||||
ctx.Package.Owner.ID,
|
||||
packages_model.TypeRpm,
|
||||
name,
|
||||
version,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var vm *rpm_module.VersionMetadata
|
||||
if pv.MetadataJSON != "" {
|
||||
if err := json.Unmarshal([]byte(pv.MetadataJSON), &vm); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
vm = &rpm_module.VersionMetadata{}
|
||||
}
|
||||
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
for _, u := range updates {
|
||||
if u == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Sanitize to remove nil elements from JSON payload
|
||||
var cleanPkgList []*rpm_module.Collection
|
||||
for _, coll := range u.PkgList {
|
||||
if coll == nil {
|
||||
continue
|
||||
}
|
||||
var cleanPackages []*rpm_module.UpdatePackage
|
||||
for _, pkg := range coll.Packages {
|
||||
if pkg == nil {
|
||||
continue
|
||||
}
|
||||
cleanPackages = append(cleanPackages, pkg)
|
||||
}
|
||||
coll.Packages = cleanPackages
|
||||
cleanPkgList = append(cleanPkgList, coll)
|
||||
}
|
||||
u.PkgList = cleanPkgList
|
||||
|
||||
found := false
|
||||
for i, existing := range vm.Updates {
|
||||
if existing.ID == u.ID {
|
||||
// Merge PkgList with deduplication
|
||||
for _, newColl := range u.PkgList {
|
||||
if newColl == nil {
|
||||
continue
|
||||
}
|
||||
collFound := false
|
||||
for j, existingColl := range existing.PkgList {
|
||||
if existingColl.Short == newColl.Short {
|
||||
// Merge packages
|
||||
for _, newPkg := range newColl.Packages {
|
||||
if newPkg == nil {
|
||||
continue
|
||||
}
|
||||
pkgFound := false
|
||||
for _, existingPkg := range existingColl.Packages {
|
||||
if existingPkg.Name == newPkg.Name &&
|
||||
existingPkg.Version == newPkg.Version &&
|
||||
existingPkg.Release == newPkg.Release &&
|
||||
existingPkg.Arch == newPkg.Arch {
|
||||
pkgFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !pkgFound {
|
||||
vm.Updates[i].PkgList[j].Packages = append(vm.Updates[i].PkgList[j].Packages, newPkg)
|
||||
}
|
||||
}
|
||||
collFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !collFound {
|
||||
vm.Updates[i].PkgList = append(vm.Updates[i].PkgList, newColl)
|
||||
}
|
||||
}
|
||||
vm.Updates[i].From = u.From
|
||||
vm.Updates[i].Status = u.Status
|
||||
vm.Updates[i].Type = u.Type
|
||||
vm.Updates[i].Version = u.Version
|
||||
vm.Updates[i].Title = u.Title
|
||||
vm.Updates[i].Severity = u.Severity
|
||||
vm.Updates[i].Description = u.Description
|
||||
vm.Updates[i].References = u.References
|
||||
vm.Updates[i].Updated = &rpm_module.DateAttr{Date: now}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
if u.Issued == nil {
|
||||
u.Issued = &rpm_module.DateAttr{Date: now}
|
||||
}
|
||||
if u.Updated == nil {
|
||||
u.Updated = &rpm_module.DateAttr{Date: now}
|
||||
}
|
||||
vm.Updates = append(vm.Updates, u)
|
||||
}
|
||||
}
|
||||
|
||||
vmBytes, err := json.Marshal(vm)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
pv.MetadataJSON = string(vmBytes)
|
||||
if err := packages_model.UpdateVersion(ctx, pv); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := rpm_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, group); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ func enumeratePackages(ctx *context.Context, filename string, pvs []*packages_mo
|
||||
})
|
||||
}
|
||||
|
||||
ctx.SetServeHeaders(&context.ServeHeaderOptions{
|
||||
ctx.SetServeHeaders(context.ServeHeaderOptions{
|
||||
Filename: filename + ".gz",
|
||||
})
|
||||
|
||||
@@ -119,7 +119,7 @@ func ServePackageSpecification(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetServeHeaders(&context.ServeHeaderOptions{
|
||||
ctx.SetServeHeaders(context.ServeHeaderOptions{
|
||||
Filename: filename,
|
||||
})
|
||||
|
||||
@@ -433,15 +433,16 @@ func makePackageVersionDependency(ctx *context.Context, version *packages_model.
|
||||
}
|
||||
|
||||
func makePackageInfo(ctx *context.Context, versions []*packages_model.PackageVersion, c *cache.EphemeralCache) (string, error) {
|
||||
ret := "---\n"
|
||||
var ret strings.Builder
|
||||
ret.WriteString("---\n")
|
||||
for _, v := range versions {
|
||||
dep, err := makePackageVersionDependency(ctx, v, c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ret += dep + "\n"
|
||||
ret.WriteString(dep + "\n")
|
||||
}
|
||||
return ret, nil
|
||||
return ret.String(), nil
|
||||
}
|
||||
|
||||
func makeGemFullFileName(gemName, version, platform string) string {
|
||||
|
||||
@@ -198,6 +198,23 @@ func PackageVersionMetadata(ctx *context.Context) {
|
||||
}
|
||||
|
||||
metadata := pd.Metadata.(*swift_module.Metadata)
|
||||
repositoryURLs := make([]string, 0, len(pd.VersionProperties))
|
||||
for _, property := range pd.VersionProperties {
|
||||
if property.Name == swift_module.PropertyRepositoryURL {
|
||||
repositoryURLs = append(repositoryURLs, property.Value)
|
||||
}
|
||||
}
|
||||
|
||||
var author *swift_module.Person
|
||||
if metadata.Author.Name != "" || metadata.Author.GivenName != "" || metadata.Author.MiddleName != "" || metadata.Author.FamilyName != "" {
|
||||
author = &swift_module.Person{
|
||||
Type: "Person",
|
||||
Name: metadata.Author.Name,
|
||||
GivenName: metadata.Author.GivenName,
|
||||
MiddleName: metadata.Author.MiddleName,
|
||||
FamilyName: metadata.Author.FamilyName,
|
||||
}
|
||||
}
|
||||
|
||||
setResponseHeaders(ctx.Resp, &headers{})
|
||||
|
||||
@@ -220,18 +237,14 @@ func PackageVersionMetadata(ctx *context.Context) {
|
||||
Keywords: metadata.Keywords,
|
||||
CodeRepository: metadata.RepositoryURL,
|
||||
License: metadata.License,
|
||||
LicenseURL: metadata.LicenseURL,
|
||||
Author: author,
|
||||
ProgrammingLanguage: swift_module.ProgrammingLanguage{
|
||||
Type: "ComputerLanguage",
|
||||
Name: "Swift",
|
||||
URL: "https://swift.org",
|
||||
},
|
||||
Author: swift_module.Person{
|
||||
Type: "Person",
|
||||
Name: metadata.Author.String(),
|
||||
GivenName: metadata.Author.GivenName,
|
||||
MiddleName: metadata.Author.MiddleName,
|
||||
FamilyName: metadata.Author.FamilyName,
|
||||
},
|
||||
RepositoryURLs: repositoryURLs,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -281,7 +294,7 @@ func DownloadManifest(ctx *context.Context) {
|
||||
filename = fmt.Sprintf("Package@swift-%s.swift", swiftVersion)
|
||||
}
|
||||
|
||||
ctx.ServeContent(strings.NewReader(m.Content), &context.ServeHeaderOptions{
|
||||
ctx.ServeContent(strings.NewReader(m.Content), context.ServeHeaderOptions{
|
||||
ContentType: "text/x-swift",
|
||||
Filename: filename,
|
||||
LastModified: pv.CreatedUnix.AsLocalTime(),
|
||||
@@ -300,7 +313,7 @@ func formFileOptionalReadCloser(ctx *context.Context, formKey string) (io.ReadCl
|
||||
|
||||
content := ctx.Req.FormValue(formKey)
|
||||
if content == "" {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the content does not exist
|
||||
}
|
||||
return io.NopCloser(strings.NewReader(content)), nil
|
||||
}
|
||||
@@ -437,7 +450,7 @@ func DownloadPackageFile(ctx *context.Context) {
|
||||
Digest: pd.Files[0].Blob.HashSHA256,
|
||||
})
|
||||
|
||||
helper.ServePackageFile(ctx, s, u, pf, &context.ServeHeaderOptions{
|
||||
helper.ServePackageFile(ctx, s, u, pf, context.ServeHeaderOptions{
|
||||
Filename: pf.Name,
|
||||
ContentType: "application/zip",
|
||||
LastModified: pf.CreatedUnix.AsLocalTime(),
|
||||
|
||||
@@ -0,0 +1,438 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/globallock"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
terraform_module "code.gitea.io/gitea/modules/packages/terraform"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
var packageNameRegex = regexp.MustCompile(`\A[-_+.\w]+\z`)
|
||||
|
||||
const (
|
||||
stateFilename = "tfstate"
|
||||
)
|
||||
|
||||
func apiError(ctx *context.Context, status int, obj any) {
|
||||
message := helper.ProcessErrorForUser(ctx, status, obj)
|
||||
ctx.PlainText(status, message)
|
||||
}
|
||||
|
||||
// GetTerraformState serves the latest version of the state
|
||||
func GetTerraformState(ctx *context.Context) {
|
||||
stateName := ctx.PathParam("name")
|
||||
pv, err := getLatestVersion(ctx, stateName)
|
||||
if errors.Is(err, packages_model.ErrPackageNotExist) {
|
||||
apiError(ctx, http.StatusNotFound, nil)
|
||||
return
|
||||
} else if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
streamState(ctx, stateName, pv.Version)
|
||||
}
|
||||
|
||||
// GetTerraformStateBySerial serves a specific version of terraform state.
|
||||
func GetTerraformStateBySerial(ctx *context.Context) {
|
||||
streamState(ctx, ctx.PathParam("name"), ctx.PathParam("serial"))
|
||||
}
|
||||
|
||||
// streamState serves the terraform state file
|
||||
func streamState(ctx *context.Context, name, serial string) {
|
||||
s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion(
|
||||
ctx,
|
||||
&packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeTerraformState,
|
||||
Name: name,
|
||||
Version: serial,
|
||||
},
|
||||
&packages_service.PackageFileInfo{
|
||||
Filename: stateFilename,
|
||||
},
|
||||
ctx.Req.Method,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.ServePackageFile(ctx, s, u, pf)
|
||||
}
|
||||
|
||||
func isValidPackageName(packageName string) bool {
|
||||
if len(packageName) == 1 && !unicode.IsLetter(rune(packageName[0])) && !unicode.IsNumber(rune(packageName[0])) {
|
||||
return false
|
||||
}
|
||||
return packageNameRegex.MatchString(packageName) && packageName != ".."
|
||||
}
|
||||
|
||||
// UploadState uploads the specific terraform package.
|
||||
func UploadState(ctx *context.Context) {
|
||||
packageName := ctx.PathParam("name")
|
||||
|
||||
if !isValidPackageName(packageName) {
|
||||
apiError(ctx, http.StatusBadRequest, errors.New("invalid package name"))
|
||||
return
|
||||
}
|
||||
lockKey := getLockKey(ctx)
|
||||
release, err := globallock.Lock(ctx, lockKey)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer release()
|
||||
|
||||
p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, packageName)
|
||||
if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if p != nil {
|
||||
// Check lock
|
||||
lock, err := terraform_module.GetLock(ctx, p.ID)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// If the state is locked, enforce the lock
|
||||
if lock.IsLocked() && lock.ID != ctx.FormString("ID") {
|
||||
ctx.JSON(http.StatusLocked, lock)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
upload, needToClose, err := ctx.UploadStream()
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if needToClose {
|
||||
defer upload.Close()
|
||||
}
|
||||
|
||||
buf, err := packages_module.CreateHashedBufferFromReader(upload)
|
||||
if err != nil {
|
||||
log.Error("Error creating hashed buffer: %v", err)
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer buf.Close()
|
||||
|
||||
state, err := terraform_module.ParseState(buf)
|
||||
if err != nil {
|
||||
log.Error("Error decoding state: %v", err)
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
_, _, err = packages_service.CreatePackageOrAddFileToExisting(
|
||||
ctx,
|
||||
&packages_service.PackageCreationInfo{
|
||||
PackageInfo: packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeTerraformState,
|
||||
Name: packageName,
|
||||
Version: strconv.FormatUint(state.Serial, 10),
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
},
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: stateFilename,
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, packages_model.ErrDuplicatePackageFile):
|
||||
apiError(ctx, http.StatusConflict, err)
|
||||
case errors.Is(err, packages_service.ErrQuotaTotalCount), errors.Is(err, packages_service.ErrQuotaTypeSize), errors.Is(err, packages_service.ErrQuotaTotalSize):
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
||||
|
||||
// DeleteStateBySerial deletes the specific serial of a terraform package as long as it's not the latest one.
|
||||
func DeleteStateBySerial(ctx *context.Context) {
|
||||
lockKey := getLockKey(ctx)
|
||||
release, err := globallock.Lock(ctx, lockKey)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer release()
|
||||
|
||||
serial := ctx.PathParam("serial")
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, ctx.PathParam("name"), serial)
|
||||
if errors.Is(err, packages_model.ErrPackageNotExist) {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
} else if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
pvLatest, err := getLatestVersion(ctx, ctx.PathParam("name"))
|
||||
if errors.Is(err, packages_model.ErrPackageNotExist) {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
} else if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if pvLatest.ID == pv.ID {
|
||||
apiError(ctx, http.StatusForbidden, errors.New("cannot delete the latest version"))
|
||||
return
|
||||
}
|
||||
|
||||
err = packages_service.DeletePackageVersionAndReferences(ctx, pv)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// DeleteState deletes the specific file of a terraform package.
|
||||
// Fails if the state is locked
|
||||
func DeleteState(ctx *context.Context) {
|
||||
packageName := ctx.PathParam("name")
|
||||
|
||||
lockKey := getLockKey(ctx)
|
||||
release, err := globallock.Lock(ctx, lockKey)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer release()
|
||||
|
||||
p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, packageName)
|
||||
if err != nil {
|
||||
if errors.Is(err, packages_model.ErrPackageNotExist) {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
lock, err := terraform_module.GetLock(ctx, p.ID)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if lock.IsLocked() {
|
||||
apiError(ctx, http.StatusLocked, errors.New("terraform state is locked"))
|
||||
return
|
||||
}
|
||||
|
||||
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
PackageID: p.ID,
|
||||
IsInternal: optional.None[bool](),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = packages_model.DeleteAllProperties(ctx, packages_model.PropertyTypePackage, p.ID)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, pv := range pvs {
|
||||
if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := packages_model.DeletePackageByID(ctx, p.ID); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// LockState locks the specific terraform state.
|
||||
// Internally, it adds a property to the package with the lock information
|
||||
// Caveat being that it allocates a package if one doesn't exist to attach the property
|
||||
func LockState(ctx *context.Context) {
|
||||
packageName := ctx.PathParam("name")
|
||||
if !isValidPackageName(packageName) {
|
||||
apiError(ctx, http.StatusBadRequest, errors.New("invalid package name"))
|
||||
return
|
||||
}
|
||||
|
||||
var reqLockInfo *terraform_module.LockInfo
|
||||
reqLockInfo, err := terraform_module.ParseLockInfo(ctx.Req.Body)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
lockKey := getLockKey(ctx)
|
||||
release, err := globallock.Lock(ctx, lockKey)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer release()
|
||||
|
||||
p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, packageName)
|
||||
if err != nil {
|
||||
// If the package doesn't exist, allocate it for the lock.
|
||||
if errors.Is(err, packages_model.ErrPackageNotExist) {
|
||||
p = &packages_model.Package{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeTerraformState,
|
||||
Name: packageName,
|
||||
LowerName: strings.ToLower(packageName),
|
||||
}
|
||||
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
currentLock, err := terraform_module.GetLock(ctx, p.ID)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if currentLock.IsLocked() {
|
||||
ctx.JSON(http.StatusLocked, currentLock)
|
||||
return
|
||||
}
|
||||
|
||||
err = terraform_module.SetLock(ctx, p.ID, reqLockInfo)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// UnlockState unlock the specific terraform state.
|
||||
// Internally, it clears the package property
|
||||
func UnlockState(ctx *context.Context) {
|
||||
packageName := ctx.PathParam("name")
|
||||
if !isValidPackageName(packageName) {
|
||||
apiError(ctx, http.StatusBadRequest, errors.New("invalid package name"))
|
||||
return
|
||||
}
|
||||
|
||||
reqLockInfo, err := terraform_module.ParseLockInfo(ctx.Req.Body)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
lockKey := getLockKey(ctx)
|
||||
release, err := globallock.Lock(ctx, lockKey)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer release()
|
||||
|
||||
p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeTerraformState, packageName)
|
||||
if err != nil {
|
||||
if errors.Is(err, packages_model.ErrPackageNotExist) {
|
||||
ctx.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
existingLock, err := terraform_module.GetLock(ctx, p.ID)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// we can bypass messing with the lock since it's empty
|
||||
if !existingLock.IsLocked() {
|
||||
ctx.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Unlocking ID must be the same as locker one.
|
||||
if existingLock.ID != reqLockInfo.ID {
|
||||
apiError(ctx, http.StatusLocked, errors.New("lock ID mismatch"))
|
||||
return
|
||||
}
|
||||
// We can clear the state if lock id matches
|
||||
err = terraform_module.RemoveLock(ctx, p.ID)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func getLatestVersion(ctx *context.Context, packageName string) (*packages_model.PackageVersion, error) {
|
||||
pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeTerraformState,
|
||||
Name: packages_model.SearchValue{ExactMatch: true, Value: packageName},
|
||||
IsInternal: optional.Some(false),
|
||||
Sort: packages_model.SortCreatedDesc,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(pvs) == 0 {
|
||||
return nil, packages_model.ErrPackageNotExist
|
||||
}
|
||||
return pvs[0], nil
|
||||
}
|
||||
|
||||
func getLockKey(ctx *context.Context) string {
|
||||
return fmt.Sprintf("terraform_lock_%d_%s", ctx.Package.Owner.ID, strings.ToLower(ctx.PathParam("name")))
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidatePackageName(t *testing.T) {
|
||||
bad := []string{
|
||||
"",
|
||||
".",
|
||||
"..",
|
||||
"-",
|
||||
"a?b",
|
||||
"a b",
|
||||
"a/b",
|
||||
}
|
||||
for _, name := range bad {
|
||||
assert.False(t, isValidPackageName(name), "bad=%q", name)
|
||||
}
|
||||
|
||||
good := []string{
|
||||
"a",
|
||||
"1",
|
||||
"a-",
|
||||
"a_b",
|
||||
"c.d+",
|
||||
}
|
||||
for _, name := range good {
|
||||
assert.True(t, isValidPackageName(name), "good=%q", name)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user