From b354f027ff03ff50cf4df8ba5a36281feb10a6c4 Mon Sep 17 00:00:00 2001 From: dancheg97 Date: Wed, 21 Jun 2023 23:30:07 +0300 Subject: [PATCH] code refactoring and package version publisher id correction --- modules/packages/arch/metadata.go | 18 ++ modules/packages/content_store.go | 15 ++ routers/api/packages/arch/arch.go | 360 ++++--------------------- routers/api/packages/arch/connector.go | 133 --------- services/packages/arch/db_manager.go | 77 ++++++ services/packages/arch/file_manager.go | 190 +++++++++++++ services/packages/arch/verificator.go | 110 ++++++++ 7 files changed, 461 insertions(+), 442 deletions(-) delete mode 100644 routers/api/packages/arch/connector.go create mode 100644 services/packages/arch/db_manager.go create mode 100644 services/packages/arch/file_manager.go create mode 100644 services/packages/arch/verificator.go diff --git a/modules/packages/arch/metadata.go b/modules/packages/arch/metadata.go index e0fba239e2..4b3631f2f2 100644 --- a/modules/packages/arch/metadata.go +++ b/modules/packages/arch/metadata.go @@ -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 +} diff --git a/modules/packages/content_store.go b/modules/packages/content_store.go index 1181fa4d52..4f53491022 100644 --- a/modules/packages/content_store.go +++ b/modules/packages/content_store.go @@ -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) +} diff --git a/routers/api/packages/arch/arch.go b/routers/api/packages/arch/arch.go index 0918d98e5b..d7b54f0638 100644 --- a/routers/api/packages/arch/arch.go +++ b/routers/api/packages/arch/arch.go @@ -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) diff --git a/routers/api/packages/arch/connector.go b/routers/api/packages/arch/connector.go deleted file mode 100644 index e19215c5a1..0000000000 --- a/routers/api/packages/arch/connector.go +++ /dev/null @@ -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 -} diff --git a/services/packages/arch/db_manager.go b/services/packages/arch/db_manager.go new file mode 100644 index 0000000000..163246a1e1 --- /dev/null +++ b/services/packages/arch/db_manager.go @@ -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 +} diff --git a/services/packages/arch/file_manager.go b/services/packages/arch/file_manager.go new file mode 100644 index 0000000000..8a935560b9 --- /dev/null +++ b/services/packages/arch/file_manager.go @@ -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 +} diff --git a/services/packages/arch/verificator.go b/services/packages/arch/verificator.go new file mode 100644 index 0000000000..4a16fcffb1 --- /dev/null +++ b/services/packages/arch/verificator.go @@ -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()) +}