0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-06-22 12:15:19 +02:00

Merge fff4ca1a690af64a1bfd30ec4e8ec85a6bb2127a into c68925152b1b6c8f92806cdbda9c4672dcc1608f

This commit is contained in:
TheFox0x7 2026-06-18 00:55:33 +09:00 committed by GitHub
commit 9ead821d41
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 175 additions and 0 deletions

View File

@ -25,6 +25,8 @@ var (
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
// ErrInvalidChart indicates an invalid chart
ErrInvalidChart = util.NewInvalidArgumentErrorf("chart is invalid")
// ErrInvalidProvenance indicates an invalid provenance file
ErrInvalidProvenance = util.NewInvalidArgumentErrorf("provenance file is invalid")
)
// Metadata for a Chart file. This models the structure of a Chart.yaml file.
@ -128,3 +130,43 @@ func ParseChartFile(r io.Reader) (*Metadata, error) {
return metadata, nil
}
// ParseProvenanceFile parses a provenance file to retrieve the metadata of a Helm chart
func ParseProvenanceFile(r io.Reader) (*Metadata, error) {
data, err := io.ReadAll(io.LimitReader(r, 1<<20))
if err != nil {
return nil, err
}
content := string(data)
// A provenance file is a PGP signed message.
// The content is between the header and the signature.
const (
header = "-----BEGIN PGP SIGNED MESSAGE-----"
separator = "...\n"
)
i := strings.Index(content, header)
if i == -1 {
// Not a PGP signed message, so it's not a valid prov file
return nil, ErrInvalidProvenance
}
content = content[i+len(header):]
// Skip Hash: ${hash} from header
// https://www.gnupg.org/gph/en/manual/x135.html
i = strings.Index(content, "\n\n")
if i != -1 {
content = content[i+2:]
}
i = strings.Index(content, separator)
if i == -1 {
// The file HAS to include the separator
// https://helm.sh/docs/topics/provenance/#the-provenance-file
return nil, ErrInvalidProvenance
}
content = content[:i] // We only need the chart.yaml part
return ParseChartFile(strings.NewReader(content))
}

View File

@ -357,6 +357,7 @@ func CommonRoutes() *web.Router {
r.Get("/index.yaml", helm.Index)
r.Get("/{filename}", helm.DownloadPackageFile)
r.Post("/api/charts", reqPackageAccess(perm.AccessModeWrite), helm.UploadPackage)
r.Post("/api/prov", reqPackageAccess(perm.AccessModeWrite), helm.UploadProvenanceFile)
}, reqPackageAccess(perm.AccessModeRead))
r.Group("/maven", func() {
r.Put("/*", reqPackageAccess(perm.AccessModeWrite), maven.UploadPackageFile)

View File

@ -209,6 +209,76 @@ func UploadPackage(ctx *context.Context) {
ctx.Status(http.StatusCreated)
}
// UploadProvenanceFile uploads and attaches the provenance file to existing helm chart
func UploadProvenanceFile(ctx *context.Context) {
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 {
apiError(ctx, http.StatusInternalServerError, err)
return
}
defer buf.Close()
metadata, err := helm_module.ParseProvenanceFile(buf)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) || errors.Is(err, io.EOF) {
apiError(ctx, http.StatusBadRequest, err)
} else {
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
if _, err := buf.Seek(0, io.SeekStart); err != nil {
apiError(ctx, http.StatusInternalServerError, err)
return
}
_, err = packages_service.AddFileToExistingPackage(
ctx,
&packages_service.PackageInfo{
Owner: ctx.Package.Owner,
PackageType: packages_model.TypeHelm,
Name: metadata.Name,
Version: metadata.Version,
},
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: createProvenanceFilename(metadata),
},
Creator: ctx.Doer,
Data: buf,
IsLead: false,
OverwriteExisting: true,
},
)
if err != nil {
switch err {
case packages_model.ErrPackageNotExist:
apiError(ctx, http.StatusNotFound, err)
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
apiError(ctx, http.StatusForbidden, err)
default:
apiError(ctx, http.StatusInternalServerError, err)
}
return
}
ctx.Status(http.StatusCreated)
}
func createFilename(metadata *helm_module.Metadata) string {
return strings.ToLower(fmt.Sprintf("%s-%s.tgz", metadata.Name, metadata.Version))
}
func createProvenanceFilename(metadata *helm_module.Metadata) string {
return strings.ToLower(fmt.Sprintf("%s-%s.tgz.prov", metadata.Name, metadata.Version))
}

View File

@ -60,8 +60,39 @@ dependencies:
zw.Close()
content := buf.Bytes()
// The signature is invalid, but the repository isn't verifying it.
provContent := `-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
` + chartContent + `
...
files:
` + filename + `: sha256:d31d2f08b885ec696c37c7f7ef106709aaf5e8575b6d3dc5d52112ed29a9cb92
-----BEGIN PGP SIGNATURE-----
wsBcBAEBCgAQBQJdy0ReCRCEO7+YH8GHYgAAfhUIADx3pHHLLINv0MFkiEYpX/Kd
nvHFBNps7hXqSocsg0a9Fi1LRAc3OpVh3knjPfHNGOy8+xOdhbqpdnB+5ty8YopI
mYMWp6cP/Mwpkt7/gP1ecWFMevicbaFH5AmJCBihBaKJE4R1IX49/wTIaLKiWkv2
cR64bmZruQPSW83UTNULtdD7kuTZXeAdTMjAK0NECsCz9/eK5AFggP4CDf7r2zNi
hZsNrzloIlBZlGGns6mUOTO42J/+JojnOLIhI3Psd0HBD2bTlsm/rSfty4yZUs7D
qtgooNdohoyGSzR5oapd7fEvauRQswJxOA0m0V+u9/eyLR0+JcYB8Udi1prnWf8=
=aHfz
-----END PGP SIGNATURE-----`
url := fmt.Sprintf("/api/packages/%s/helm", user.Name)
t.Run("UploadProvFileWithoutChart", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
provURL := url + "/api/prov"
// Attempt to upload provenance file without chart to back it.
req := NewRequestWithBody(t, "POST", provURL, bytes.NewReader([]byte(provContent))).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusNotFound)
})
t.Run("Upload", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
@ -95,6 +126,37 @@ dependencies:
req = NewRequestWithBody(t, "POST", uploadURL, bytes.NewReader(content)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
provURL := url + "/api/prov"
// Upload Provenance file
req = NewRequestWithBody(t, "POST", provURL, bytes.NewReader([]byte(provContent))).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
pvs, err = packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeHelm)
assert.NoError(t, err)
assert.Len(t, pvs, 1)
pfs, err = packages.GetFilesByVersionID(t.Context(), pvs[0].ID)
assert.NoError(t, err)
assert.Len(t, pfs, 2)
var provFile *packages.PackageFile
for _, pf := range pfs {
if pf.Name == filename+".prov" {
provFile = pf
break
}
}
assert.NotNil(t, provFile)
assert.False(t, provFile.IsLead)
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s.prov", url, filename)).
AddBasicAuth(user.Name)
resp := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, provContent, resp.Body.String())
})
t.Run("Download", func(t *testing.T) {