mirror of
https://github.com/go-gitea/gitea.git
synced 2024-09-01 14:56:30 +00:00
code refactoring and package version publisher id correction
This commit is contained in:
parent
808a7aed67
commit
b354f027ff
@ -258,3 +258,21 @@ func PackDb(src, dst string) error {
|
||||
}
|
||||
return os.Symlink(dst, symlink)
|
||||
}
|
||||
|
||||
// Join database or package names to prevent collisions with same packages in
|
||||
// different user spaces. Skips empty strings and returns name joined with
|
||||
// dots.
|
||||
func Join(s ...string) string {
|
||||
rez := ""
|
||||
for i, v := range s {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
if i+1 == len(s) {
|
||||
rez += v
|
||||
continue
|
||||
}
|
||||
rez += v + "."
|
||||
}
|
||||
return rez
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
@ -63,3 +64,17 @@ func RelativePathToKey(relativePath string) (BlobHash256Key, error) {
|
||||
|
||||
return BlobHash256Key(parts[2]), nil
|
||||
}
|
||||
|
||||
// Save data with specified string key.
|
||||
func (s *ContentStore) SaveStrBytes(key string, data []byte) error {
|
||||
return s.Save(BlobHash256Key(key), bytes.NewReader(data), int64(len(data)))
|
||||
}
|
||||
|
||||
// Get data related to provided key.
|
||||
func (s *ContentStore) GetStrBytes(key string) ([]byte, error) {
|
||||
obj, err := s.Get(BlobHash256Key(key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return io.ReadAll(obj)
|
||||
}
|
||||
|
@ -4,43 +4,27 @@
|
||||
package arch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
arch_module "code.gitea.io/gitea/modules/packages/arch"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/google/uuid"
|
||||
arch_service "code.gitea.io/gitea/services/packages/arch"
|
||||
)
|
||||
|
||||
// Push new package to arch package registry.
|
||||
func Push(ctx *context.Context) {
|
||||
// Creating connector that will help with keys/blobs.
|
||||
connector := Connector{ctx: ctx}
|
||||
|
||||
// Getting some information related to package from headers.
|
||||
filename := ctx.Req.Header.Get("filename")
|
||||
email := ctx.Req.Header.Get("email")
|
||||
sign := ctx.Req.Header.Get("sign")
|
||||
owner := ctx.Req.Header.Get("owner")
|
||||
distro := ctx.Req.Header.Get("distro")
|
||||
var (
|
||||
filename = ctx.Req.Header.Get("filename")
|
||||
email = ctx.Req.Header.Get("email")
|
||||
sign = ctx.Req.Header.Get("sign")
|
||||
owner = ctx.Req.Header.Get("owner")
|
||||
distro = ctx.Req.Header.Get("distro")
|
||||
)
|
||||
|
||||
// Decoding package signature.
|
||||
sigdata, err := hex.DecodeString(sign)
|
||||
@ -48,49 +32,6 @@ func Push(ctx *context.Context) {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
pgpsig := crypto.NewPGPSignature(sigdata)
|
||||
|
||||
// Validating that user is allowed to push to specified namespace.
|
||||
err = connector.ValidateNamespace(owner, email)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Getting GPG keys related to specific user. After keys have been recieved,
|
||||
// this function will find one key related to email provided in request.
|
||||
armoredKeys, err := connector.GetValidKeys(email)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
var matchedKeyring *crypto.KeyRing
|
||||
for _, armor := range armoredKeys {
|
||||
pgpkey, err := crypto.NewKeyFromArmored(armor)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
keyring, err := crypto.NewKeyRing(pgpkey)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
for _, idnt := range keyring.GetIdentities() {
|
||||
if idnt.Email == email {
|
||||
matchedKeyring = keyring
|
||||
break
|
||||
}
|
||||
}
|
||||
if matchedKeyring != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if matchedKeyring == nil {
|
||||
msg := "GPG key related to " + email + " not found"
|
||||
apiError(ctx, http.StatusBadRequest, msg)
|
||||
return
|
||||
}
|
||||
|
||||
// Read package to memory and create plain GPG message to validate signature.
|
||||
pkgdata, err := io.ReadAll(ctx.Req.Body)
|
||||
@ -100,23 +41,19 @@ func Push(ctx *context.Context) {
|
||||
}
|
||||
defer ctx.Req.Body.Close()
|
||||
|
||||
pgpmes := crypto.NewPlainMessage(pkgdata)
|
||||
|
||||
// Validate package signature with user's GPG key related to his email.
|
||||
err = matchedKeyring.VerifyDetached(pgpmes, pgpsig, crypto.GetUnixTime())
|
||||
// Get user and organization owning arch package.
|
||||
user, org, err := arch_service.IdentifyOwner(ctx, owner, email)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusUnauthorized, "unable to validate package signature")
|
||||
apiError(ctx, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create temporary directory for arch database operations.
|
||||
tmpdir := path.Join(setting.Repository.Upload.TempPath, uuid.New().String())
|
||||
err = os.MkdirAll(tmpdir, os.ModePerm)
|
||||
// Validate package signature with user's GnuPG key.
|
||||
err = arch_service.ValidatePackageSignature(ctx, pkgdata, sigdata, user)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, "unable to create tmp path")
|
||||
apiError(ctx, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
// Parse metadata contained in arch package archive.
|
||||
md, err := arch_module.EjectMetadata(filename, setting.Domain, pkgdata)
|
||||
@ -125,211 +62,43 @@ func Push(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Arch database related filenames, pathes and folders.
|
||||
dbname := Join(owner, distro, setting.Domain, "db.tar.gz")
|
||||
dbpath := path.Join(tmpdir, dbname)
|
||||
dbfolder := path.Join(tmpdir, dbname) + ".folder"
|
||||
dbsymlink := strings.TrimSuffix(dbname, ".tar.gz")
|
||||
dbsymlinkpath := path.Join(tmpdir, dbsymlink)
|
||||
|
||||
// Get existing arch package database, related to specific userspace from
|
||||
// file storage, and save it on disk, then unpack it's contents to related
|
||||
// folder. If database is not found in storage, create empty directory to
|
||||
// store package related information.
|
||||
dbdata, err := connector.Get(dbname)
|
||||
if err == nil {
|
||||
err = os.WriteFile(dbpath, dbdata, os.ModePerm)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
err = arch_module.UnpackDb(dbpath, dbfolder)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
err = os.MkdirAll(dbfolder, os.ModePerm)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Update database folder with metadata for new package.
|
||||
err = md.PutToDb(dbfolder, os.ModePerm)
|
||||
// Get package property from DB if exists/create new one.
|
||||
dbpkg, err := arch_service.CreateGetPackage(ctx, org, md.Name)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create database archive and related symlink.
|
||||
err = arch_module.PackDb(dbfolder, dbpath)
|
||||
// Create or get package version from DB if exists/create new one.
|
||||
dbpkgver, err := arch_service.CreateGetPackageVersion(ctx, md, dbpkg, user)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Save namespace related arch repository database.
|
||||
f, err := os.Open(dbpath)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
dbfi, err := f.Stat()
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
err = connector.Save(dbname, f, dbfi.Size())
|
||||
// Automatically connect repository for provided package if name matched.
|
||||
err = arch_service.RepositoryAutoconnect(ctx, owner, md.Name, dbpkg)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Save namespace related arch repository db archive.
|
||||
f, err = os.Open(dbsymlinkpath)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
dbarchivefi, err := f.Stat()
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
err = connector.Save(dbsymlink, f, dbarchivefi.Size())
|
||||
// Save package file data to gitea storage and update database.
|
||||
err = arch_service.SavePackageFile(ctx, pkgdata, distro, filename, dbpkgver.ID)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create package in database.
|
||||
pkg, err := packages_model.TryInsertPackage(ctx, &packages_model.Package{
|
||||
OwnerID: connector.org.ID,
|
||||
Type: packages_model.TypeArch,
|
||||
Name: md.Name,
|
||||
LowerName: strings.ToLower(md.Name),
|
||||
})
|
||||
if errors.Is(err, packages_model.ErrDuplicatePackage) {
|
||||
pkg, err = packages_model.GetPackageByName(
|
||||
ctx, connector.org.ID,
|
||||
packages_model.TypeArch, md.Name,
|
||||
)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Save package signature data to gitea storage and update database.
|
||||
err = arch_service.SavePackageFile(ctx, sigdata, distro, filename+".sig", dbpkgver.ID)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if repository for package with provided owner exists.
|
||||
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, md.Name)
|
||||
if err == nil {
|
||||
err = packages_model.SetRepositoryLink(ctx, pkg.ID, repo.ID)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Create new package version in database.
|
||||
rawjsonmetadata, err := json.Marshal(&md)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ver, err := packages_model.GetOrInsertVersion(ctx, &packages_model.PackageVersion{
|
||||
PackageID: pkg.ID,
|
||||
CreatorID: connector.org.ID,
|
||||
Version: md.Version,
|
||||
LowerVersion: strings.ToLower(md.Version),
|
||||
CreatedUnix: timeutil.TimeStampNow(),
|
||||
MetadataJSON: string(rawjsonmetadata),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, packages_model.ErrDuplicatePackageVersion) {
|
||||
apiError(ctx, http.StatusConflict, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create package blob and db file for package file.
|
||||
pkgreader := bytes.NewReader(pkgdata)
|
||||
fbuf, err := packages_module.CreateHashedBufferFromReader(pkgreader)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer fbuf.Close()
|
||||
|
||||
filepb, ok, err := packages_model.GetOrInsertBlob(
|
||||
ctx, packages_service.NewPackageBlob(fbuf),
|
||||
)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, fmt.Errorf("%v %t", err, ok))
|
||||
return
|
||||
}
|
||||
err = connector.Save(filepb.HashSHA256, fbuf, filepb.Size)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = packages_model.TryInsertFile(ctx, &packages_model.PackageFile{
|
||||
VersionID: ver.ID,
|
||||
BlobID: filepb.ID,
|
||||
Name: filename,
|
||||
LowerName: strings.ToLower(filename),
|
||||
CompositeKey: distro + "-" + filename,
|
||||
IsLead: true,
|
||||
CreatedUnix: timeutil.TimeStampNow(),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create package blob for package signature.
|
||||
sigreader := bytes.NewReader(sigdata)
|
||||
sbuf, err := packages_module.CreateHashedBufferFromReader(sigreader)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer fbuf.Close()
|
||||
|
||||
sigpb, ok, err := packages_model.GetOrInsertBlob(
|
||||
ctx, packages_service.NewPackageBlob(sbuf),
|
||||
)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, fmt.Errorf("%v %t", err, ok))
|
||||
return
|
||||
}
|
||||
err = connector.Save(sigpb.HashSHA256, sbuf, sigpb.Size)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = packages_model.TryInsertFile(ctx, &packages_model.PackageFile{
|
||||
VersionID: ver.ID,
|
||||
BlobID: sigpb.ID,
|
||||
Name: filename + ".sig",
|
||||
LowerName: strings.ToLower(filename + ".sig"),
|
||||
CompositeKey: distro + "-" + filename + ".sig",
|
||||
IsLead: false,
|
||||
CreatedUnix: timeutil.TimeStampNow(),
|
||||
})
|
||||
// Update pacman databases with new package.
|
||||
err = arch_service.UpdatePacmanDatabases(ctx, md, distro, owner)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
@ -340,68 +109,41 @@ func Push(ctx *context.Context) {
|
||||
|
||||
// Get file from arch package registry.
|
||||
func Get(ctx *context.Context) {
|
||||
filename := ctx.Params("file")
|
||||
owner := ctx.Params("owner")
|
||||
distro := ctx.Params("distro")
|
||||
// arch := ctx.Params("arch")
|
||||
var (
|
||||
file = ctx.Params("file")
|
||||
owner = ctx.Params("owner")
|
||||
distro = ctx.Params("distro")
|
||||
arch = ctx.Params("arch")
|
||||
)
|
||||
|
||||
cs := packages_module.NewContentStore()
|
||||
|
||||
if strings.HasSuffix(filename, "tar.zst") || strings.HasSuffix(filename, "zst.sig") {
|
||||
db := db.GetEngine(ctx)
|
||||
|
||||
pkgfile := &packages_model.PackageFile{
|
||||
CompositeKey: distro + "-" + filename,
|
||||
}
|
||||
ok, err := db.Get(pkgfile)
|
||||
if err != nil || !ok {
|
||||
apiError(
|
||||
ctx, http.StatusInternalServerError,
|
||||
fmt.Errorf("%+v %t", err, ok),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
blob, err := packages_model.GetBlobByID(ctx, pkgfile.BlobID)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
obj, err := cs.Get(packages_module.BlobHash256Key(blob.HashSHA256))
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(obj)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = ctx.Resp.Write(data)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(filename, ".db.tar.gz") || strings.HasSuffix(filename, ".db") {
|
||||
filename = strings.TrimPrefix(filename, owner+".")
|
||||
obj, err := cs.Get(packages_module.BlobHash256Key(Join(owner, distro, filename)))
|
||||
// Packages are stored in different way from pacman databases, and loaded
|
||||
// with LoadPackageFile function.
|
||||
if strings.HasSuffix(file, "tar.zst") || strings.HasSuffix(file, "zst.sig") {
|
||||
pkgdata, err := arch_service.LoadPackageFile(ctx, distro, file)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(obj)
|
||||
_, err = ctx.Resp.Write(pkgdata)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Pacman databases are stored directly in gitea file storage and could be
|
||||
// loaded with name as a key.
|
||||
if strings.HasSuffix(file, ".db.tar.gz") || strings.HasSuffix(file, ".db") {
|
||||
data, err := arch_service.LoadPacmanDatabase(ctx, owner, distro, arch, file)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = ctx.Resp.Write(data)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
|
@ -1,133 +0,0 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package arch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"code.gitea.io/gitea/models/asymkey"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
organization_model "code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
)
|
||||
|
||||
// Connector helps to retrieve GPG keys related to package validation and
|
||||
// manage blobs related to specific user spaces:
|
||||
// 1 - Check if user is allowed to push package to specific namespace.
|
||||
// 2 - Retrieving GPG keys related to provided email.
|
||||
// 3 - Get/put arch arch package/signature/database files to connected file
|
||||
// storage.
|
||||
type Connector struct {
|
||||
ctx *context.Context
|
||||
user *user_model.User
|
||||
org *organization_model.Organization
|
||||
}
|
||||
|
||||
// This function will find user related to provided email adress and check if
|
||||
// he is able to push packages to provided namespace (user/organization/or
|
||||
// empty namespace allowed for admin users).
|
||||
func (c *Connector) ValidateNamespace(namespace, email string) error {
|
||||
var err error
|
||||
c.user, err = user_model.GetUserByEmail(c.ctx, email)
|
||||
if err != nil {
|
||||
log.Error("unable to get user with email: %s %v", email, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if namespace == "" && c.user.IsAdmin {
|
||||
c.org = (*organization_model.Organization)(c.user)
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.user.Name != namespace && c.org == nil {
|
||||
c.org, err = organization_model.GetOrgByName(c.ctx, namespace)
|
||||
if err != nil {
|
||||
log.Error("unable to organization: %s %v", namespace, err)
|
||||
return err
|
||||
}
|
||||
ismember, err := c.org.IsOrgMember(c.user.ID)
|
||||
if err != nil {
|
||||
log.Error(
|
||||
"unable to check if user belongs to organization: %s %s %v",
|
||||
c.user.Name, email, err,
|
||||
)
|
||||
return err
|
||||
}
|
||||
if !ismember {
|
||||
log.Error("user %s is not member of organization: %s", c.user.Name, email)
|
||||
return errors.New("user is not member of organization: " + namespace)
|
||||
}
|
||||
} else {
|
||||
c.org = (*organization_model.Organization)(c.user)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// This function will try to find user related to specific email. And check
|
||||
// that user is allowed to push to 'owner' namespace (package owner, could
|
||||
// be empty, user or organization).
|
||||
// After namespace check, this function
|
||||
func (c *Connector) GetValidKeys(email string) ([]string, error) {
|
||||
keys, err := asymkey.ListGPGKeys(c.ctx, c.user.ID, db.ListOptions{
|
||||
ListAll: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("unable to get keys related to user: %v", err)
|
||||
return nil, errors.New("unable to get public keys")
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
log.Error("no keys related to user")
|
||||
return nil, errors.New("no keys for user with email: " + email)
|
||||
}
|
||||
|
||||
var keyarmors []string
|
||||
for _, key := range keys {
|
||||
k, err := asymkey.GetGPGImportByKeyID(key.KeyID)
|
||||
if err != nil {
|
||||
log.Error("unable to import GPG key by ID: %v", err)
|
||||
return nil, errors.New("internal error")
|
||||
}
|
||||
keyarmors = append(keyarmors, k.Content)
|
||||
}
|
||||
|
||||
return keyarmors, nil
|
||||
}
|
||||
|
||||
// Get specific file content from content storage.
|
||||
func (c *Connector) Get(key string) ([]byte, error) {
|
||||
cs := packages_module.NewContentStore()
|
||||
obj, err := cs.Get(packages_module.BlobHash256Key(key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return io.ReadAll(obj)
|
||||
}
|
||||
|
||||
// Save contents related to specific arch package.
|
||||
func (c *Connector) Save(key string, content io.Reader, size int64) error {
|
||||
cs := packages_module.NewContentStore()
|
||||
return cs.Save(packages_module.BlobHash256Key(key), content, size)
|
||||
}
|
||||
|
||||
// Join database or package names to prevent collisions with same packages in
|
||||
// different user spaces. Skips empty strings and returns name joined with
|
||||
// dots.
|
||||
func Join(s ...string) string {
|
||||
rez := ""
|
||||
for i, v := range s {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
if i+1 == len(s) {
|
||||
rez += v
|
||||
continue
|
||||
}
|
||||
rez += v + "."
|
||||
}
|
||||
return rez
|
||||
}
|
77
services/packages/arch/db_manager.go
Normal file
77
services/packages/arch/db_manager.go
Normal file
@ -0,0 +1,77 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package arch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
org "code.gitea.io/gitea/models/organization"
|
||||
pkg "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/packages/arch"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
)
|
||||
|
||||
// This function will create new package in database, if it does not exist it
|
||||
// will get existing and return it back to user.
|
||||
func CreateGetPackage(ctx *context.Context, o *org.Organization, name string) (*pkg.Package, error) {
|
||||
pack, err := pkg.TryInsertPackage(ctx, &pkg.Package{
|
||||
OwnerID: o.ID,
|
||||
Type: pkg.TypeArch,
|
||||
Name: name,
|
||||
LowerName: strings.ToLower(name),
|
||||
})
|
||||
if errors.Is(err, pkg.ErrDuplicatePackage) {
|
||||
pack, err = pkg.GetPackageByName(ctx, o.ID, pkg.TypeArch, name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get package %s in organization %s", name, o.Name)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pack, nil
|
||||
}
|
||||
|
||||
// This function will create new version for package, or find and return existing.
|
||||
func CreateGetPackageVersion(ctx *context.Context, md *arch.Metadata, p *pkg.Package, u *user.User) (*pkg.PackageVersion, error) {
|
||||
rawjsonmetadata, err := json.Marshal(&md)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ver, err := pkg.GetOrInsertVersion(ctx, &pkg.PackageVersion{
|
||||
PackageID: p.ID,
|
||||
CreatorID: u.ID,
|
||||
Version: md.Version,
|
||||
LowerVersion: strings.ToLower(md.Version),
|
||||
CreatedUnix: timeutil.TimeStampNow(),
|
||||
MetadataJSON: string(rawjsonmetadata),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pkg.ErrDuplicatePackageVersion) {
|
||||
return ver, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return ver, nil
|
||||
}
|
||||
|
||||
// Automatically connect repository to pushed package, if package with provided
|
||||
// with provided name exists in namespace scope.
|
||||
func RepositoryAutoconnect(ctx *context.Context, owner, repository string, p *pkg.Package) error {
|
||||
repo, err := repo.GetRepositoryByOwnerAndName(ctx, owner, repository)
|
||||
if err == nil {
|
||||
err = pkg.SetRepositoryLink(ctx, p.ID, repo.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
190
services/packages/arch/file_manager.go
Normal file
190
services/packages/arch/file_manager.go
Normal file
@ -0,0 +1,190 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package arch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
pkg_mdl "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/packages"
|
||||
"code.gitea.io/gitea/modules/packages/arch"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
pkg_svc "code.gitea.io/gitea/services/packages"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Save package file to content store for the provided version id and specified distribution.
|
||||
func SavePackageFile(ctx *context.Context, data []byte, distro, filename string, pkgverid int64) error {
|
||||
buf, err := packages.CreateHashedBufferFromReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer buf.Close()
|
||||
|
||||
blob, _, err := pkg_mdl.GetOrInsertBlob(ctx, pkg_svc.NewPackageBlob(buf))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cs := packages.NewContentStore()
|
||||
err = cs.Save(packages.BlobHash256Key(blob.HashSHA256), buf, blob.Size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = pkg_mdl.TryInsertFile(ctx, &pkg_mdl.PackageFile{
|
||||
VersionID: pkgverid,
|
||||
BlobID: blob.ID,
|
||||
Name: filename,
|
||||
LowerName: strings.ToLower(filename),
|
||||
CompositeKey: distro + "-" + filename,
|
||||
CreatedUnix: timeutil.TimeStampNow(),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// Get data related to provided file name and distribution.
|
||||
func LoadPackageFile(ctx *context.Context, distro, file string) ([]byte, error) {
|
||||
db := db.GetEngine(ctx)
|
||||
|
||||
pkgfile := &pkg_mdl.PackageFile{CompositeKey: distro + "-" + file}
|
||||
|
||||
ok, err := db.Get(pkgfile)
|
||||
if err != nil || !ok {
|
||||
return nil, fmt.Errorf("%+v %t", err, ok)
|
||||
}
|
||||
|
||||
blob, err := pkg_mdl.GetBlobByID(ctx, pkgfile.BlobID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cs := packages.NewContentStore()
|
||||
|
||||
obj, err := cs.Get(packages.BlobHash256Key(blob.HashSHA256))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return io.ReadAll(obj)
|
||||
}
|
||||
|
||||
// Get data related to pacman database file or symlink.
|
||||
func LoadPacmanDatabase(ctx *context.Context, owner, distro, architecture, file string) ([]byte, error) {
|
||||
|
||||
cs := packages.NewContentStore()
|
||||
|
||||
file = strings.TrimPrefix(file, owner+".")
|
||||
|
||||
obj, err := cs.Get(packages.BlobHash256Key(arch.Join(owner, distro, architecture, file)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return io.ReadAll(obj)
|
||||
}
|
||||
|
||||
// This function will update information about package in related pacman databases
|
||||
// or create them if they do not exist.
|
||||
func UpdatePacmanDatabases(ctx *context.Context, md *arch.Metadata, distro, owner string) error {
|
||||
// Create temporary directory for arch database operations.
|
||||
tmpdir := path.Join(setting.Repository.Upload.TempPath, uuid.New().String())
|
||||
err := os.MkdirAll(tmpdir, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
// If architecure is not specified or any, package will be automatically
|
||||
// saved to databases with most popular architectures.
|
||||
var architectures = md.Arch
|
||||
if len(md.Arch) == 0 || md.Arch[0] == "any" {
|
||||
architectures = []string{
|
||||
"x86_64", "arm", "i686", "pentium4",
|
||||
"armv7h", "armv6h", "aarch64", "riscv64",
|
||||
}
|
||||
}
|
||||
|
||||
cs := packages.NewContentStore()
|
||||
|
||||
for _, architecture := range architectures {
|
||||
var (
|
||||
db = arch.Join(owner, distro, architecture, setting.Domain, "db.tar.gz")
|
||||
dbpth = path.Join(tmpdir, db)
|
||||
dbf = path.Join(tmpdir, db) + ".folder"
|
||||
sbsl = strings.TrimSuffix(db, ".tar.gz")
|
||||
slpth = path.Join(tmpdir, sbsl)
|
||||
)
|
||||
|
||||
// Get existing pacman database, or create empty folder for it.
|
||||
dbdata, err := cs.GetStrBytes(db)
|
||||
if err == nil {
|
||||
err = os.WriteFile(dbpth, dbdata, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = arch.UnpackDb(dbpth, dbf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
err = os.MkdirAll(dbf, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Update database folder with metadata for new package.
|
||||
err = md.PutToDb(dbf, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create database archive and related symlink.
|
||||
err = arch.PackDb(dbf, dbpth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save database file.
|
||||
f, err := os.Open(dbpth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
dbfi, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = cs.Save(packages.BlobHash256Key(db), f, dbfi.Size())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save database symlink file.
|
||||
f, err = os.Open(slpth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
dbarchivefi, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = cs.Save(packages.BlobHash256Key(sbsl), f, dbarchivefi.Size())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
110
services/packages/arch/verificator.go
Normal file
110
services/packages/arch/verificator.go
Normal file
@ -0,0 +1,110 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package arch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/asymkey"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
org "code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
)
|
||||
|
||||
type IdentidyOwnerParameters struct {
|
||||
*context.Context
|
||||
Owner string
|
||||
Email string
|
||||
}
|
||||
|
||||
// This function will find user related to provided email adress and check if
|
||||
// he is able to push packages to provided namespace (user/organization/or
|
||||
// empty namespace allowed for admin users). Function will return user making
|
||||
// operation, organization or user owning the package.
|
||||
func IdentifyOwner(ctx *context.Context, owner, email string) (*user.User, *org.Organization, error) {
|
||||
u, err := user.GetUserByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to find user with email %s, %v", email, err)
|
||||
}
|
||||
|
||||
if owner == "" && u.IsAdmin {
|
||||
return u, (*org.Organization)(u), nil
|
||||
}
|
||||
|
||||
if owner == u.Name {
|
||||
return u, (*org.Organization)(u), nil
|
||||
}
|
||||
|
||||
if u.Name != owner {
|
||||
org, err := org.GetOrgByName(ctx, owner)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to get organization: %s, %v", owner, err)
|
||||
}
|
||||
ismember, err := org.IsOrgMember(u.ID)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to check if user %s belongs to organization %s: %v", u.Name, org.Name, err)
|
||||
}
|
||||
if !ismember {
|
||||
return nil, nil, fmt.Errorf("user %s is not member of organization %s", u.Name, org.Name)
|
||||
}
|
||||
return u, org, nil
|
||||
}
|
||||
return nil, nil, fmt.Errorf("unknown package owner")
|
||||
}
|
||||
|
||||
// Validate package signature with owner's GnuPG keys stored in gitea's database.
|
||||
func ValidatePackageSignature(ctx *context.Context, pkg, sign []byte, u *user.User) error {
|
||||
keys, err := asymkey.ListGPGKeys(ctx, u.ID, db.ListOptions{
|
||||
ListAll: true,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.New("unable to get public keys")
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return errors.New("no keys for user with email: " + u.Email)
|
||||
}
|
||||
|
||||
var keyarmors []string
|
||||
for _, key := range keys {
|
||||
k, err := asymkey.GetGPGImportByKeyID(key.KeyID)
|
||||
if err != nil {
|
||||
return errors.New("unable to import GPG key armor")
|
||||
}
|
||||
keyarmors = append(keyarmors, k.Content)
|
||||
}
|
||||
|
||||
var matchedKeyring *crypto.KeyRing
|
||||
for _, armor := range keyarmors {
|
||||
pgpkey, err := crypto.NewKeyFromArmored(armor)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get keys for %s: %v", u.Name, err)
|
||||
}
|
||||
keyring, err := crypto.NewKeyRing(pgpkey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to form keyring %s: %v", u.Name, err)
|
||||
}
|
||||
for _, idnt := range keyring.GetIdentities() {
|
||||
if idnt.Email == u.Email {
|
||||
matchedKeyring = keyring
|
||||
break
|
||||
}
|
||||
}
|
||||
if matchedKeyring != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if matchedKeyring == nil {
|
||||
return fmt.Errorf("GPG key related to %s not found", u.Email)
|
||||
}
|
||||
|
||||
var (
|
||||
pgpmes = crypto.NewPlainMessage(pkg)
|
||||
pgpsig = crypto.NewPGPSignature(sign)
|
||||
)
|
||||
|
||||
return matchedKeyring.VerifyDetached(pgpmes, pgpsig, crypto.GetUnixTime())
|
||||
}
|
Loading…
Reference in New Issue
Block a user