package boltdb import ( "context" "encoding/json" "errors" "fmt" "strings" "time" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" "go.etcd.io/bbolt" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" zerr "zotregistry.dev/zot/errors" "zotregistry.dev/zot/pkg/api/constants" zcommon "zotregistry.dev/zot/pkg/common" "zotregistry.dev/zot/pkg/log" "zotregistry.dev/zot/pkg/meta/common" mConvert "zotregistry.dev/zot/pkg/meta/convert" proto_go "zotregistry.dev/zot/pkg/meta/proto/gen" mTypes "zotregistry.dev/zot/pkg/meta/types" "zotregistry.dev/zot/pkg/meta/version" reqCtx "zotregistry.dev/zot/pkg/requestcontext" ) type BoltDB struct { DB *bbolt.DB Patches []func(DB *bbolt.DB) error imgTrustStore mTypes.ImageTrustStore Log log.Logger } func New(boltDB *bbolt.DB, log log.Logger) (*BoltDB, error) { err := boltDB.Update(func(transaction *bbolt.Tx) error { versionBuck, err := transaction.CreateBucketIfNotExists([]byte(VersionBucket)) if err != nil { return err } err = versionBuck.Put([]byte(version.DBVersionKey), []byte(version.CurrentVersion)) if err != nil { return err } _, err = transaction.CreateBucketIfNotExists([]byte(UserDataBucket)) if err != nil { return err } _, err = transaction.CreateBucketIfNotExists([]byte(UserAPIKeysBucket)) if err != nil { return err } _, err = transaction.CreateBucketIfNotExists([]byte(ImageMetaBuck)) if err != nil { return err } _, err = transaction.CreateBucketIfNotExists([]byte(RepoMetaBuck)) if err != nil { return err } repoBlobsBuck, err := transaction.CreateBucketIfNotExists([]byte(RepoBlobsBuck)) if err != nil { return err } _, err = repoBlobsBuck.CreateBucketIfNotExists([]byte(RepoLastUpdatedBuck)) if err != nil { return err } return nil }) if err != nil { return nil, err } return &BoltDB{ DB: boltDB, Patches: version.GetBoltDBPatches(), imgTrustStore: nil, Log: log, }, nil } func (bdw *BoltDB) GetAllRepoNames() ([]string, error) { repoNames := []string{} err := bdw.DB.View(func(tx *bbolt.Tx) error { repoMetaBuck := tx.Bucket([]byte(RepoMetaBuck)) return repoMetaBuck.ForEach(func(repo, _ []byte) error { repoNames = append(repoNames, string(repo)) return nil }) }) return repoNames, err } func (bdw *BoltDB) GetRepoLastUpdated(repo string) time.Time { lastUpdated := time.Time{} err := bdw.DB.View(func(tx *bbolt.Tx) error { repoBlobsBuck := tx.Bucket([]byte(RepoBlobsBuck)) lastUpdatedBuck := repoBlobsBuck.Bucket([]byte(RepoLastUpdatedBuck)) lastUpdatedBlob := lastUpdatedBuck.Get([]byte(repo)) if len(lastUpdatedBlob) == 0 { return zerr.ErrRepoMetaNotFound } protoTime := ×tamppb.Timestamp{} err := proto.Unmarshal(lastUpdatedBlob, protoTime) if err != nil { return err } lastUpdated = *mConvert.GetTime(protoTime) return nil }) if err != nil { return time.Time{} } return lastUpdated } func (bdw *BoltDB) SetImageMeta(digest godigest.Digest, imageMeta mTypes.ImageMeta) error { err := bdw.DB.Update(func(tx *bbolt.Tx) error { buck := tx.Bucket([]byte(ImageMetaBuck)) protoImageMeta := &proto_go.ImageMeta{} switch imageMeta.MediaType { case ispec.MediaTypeImageManifest: manifest := imageMeta.Manifests[0] protoImageMeta = mConvert.GetProtoImageManifestData(manifest.Manifest, manifest.Config, manifest.Size, manifest.Digest.String()) case ispec.MediaTypeImageIndex: protoImageMeta = mConvert.GetProtoImageIndexMeta(*imageMeta.Index, imageMeta.Size, imageMeta.Digest.String()) } pImageMetaBlob, err := proto.Marshal(protoImageMeta) if err != nil { return fmt.Errorf("failed to calculate blob for manifest with digest %s %w", digest, err) } err = buck.Put([]byte(digest), pImageMetaBlob) if err != nil { return fmt.Errorf("failed to set manifest data with for digest %s %w", digest, err) } return nil }) return err } func (bdw *BoltDB) SetRepoReference(ctx context.Context, repo string, reference string, imageMeta mTypes.ImageMeta, ) error { if err := common.ValidateRepoReferenceInput(repo, reference, imageMeta.Digest); err != nil { return err } var userid string userAc, err := reqCtx.UserAcFromContext(ctx) if err == nil { userid = userAc.GetUsername() } err = bdw.DB.Update(func(tx *bbolt.Tx) error { repoBuck := tx.Bucket([]byte(RepoMetaBuck)) repoBlobsBuck := tx.Bucket([]byte(RepoBlobsBuck)) repoLastUpdatedBuck := repoBlobsBuck.Bucket([]byte(RepoLastUpdatedBuck)) imageBuck := tx.Bucket([]byte(ImageMetaBuck)) // 1. Add image data to db if needed protoImageMeta := mConvert.GetProtoImageMeta(imageMeta) imageMetaBlob, err := proto.Marshal(protoImageMeta) if err != nil { return err } err = imageBuck.Put([]byte(imageMeta.Digest), imageMetaBlob) if err != nil { return err } protoRepoMeta, err := getProtoRepoMeta(repo, repoBuck) if err != nil && !errors.Is(err, zerr.ErrRepoMetaNotFound) { return err } // 2. Referrers if subject := mConvert.GetImageSubject(protoImageMeta); subject != nil { refInfo := &proto_go.ReferrersInfo{} if protoRepoMeta.Referrers[subject.Digest.String()] != nil { refInfo = protoRepoMeta.Referrers[subject.Digest.String()] } foundReferrer := false for i := range refInfo.List { if refInfo.List[i].Digest == mConvert.GetImageDigestStr(protoImageMeta) { foundReferrer = true refInfo.List[i].Count += 1 break } } if !foundReferrer { refInfo.List = append(refInfo.List, &proto_go.ReferrerInfo{ Count: 1, MediaType: protoImageMeta.MediaType, Digest: mConvert.GetImageDigestStr(protoImageMeta), ArtifactType: mConvert.GetImageArtifactType(protoImageMeta), Size: mConvert.GetImageManifestSize(protoImageMeta), Annotations: mConvert.GetImageAnnotations(protoImageMeta), }) } protoRepoMeta.Referrers[subject.Digest.String()] = refInfo } // 3. Update tag if !common.ReferenceIsDigest(reference) { protoRepoMeta.Tags[reference] = &proto_go.TagDescriptor{ Digest: imageMeta.Digest.String(), MediaType: imageMeta.MediaType, } } if _, ok := protoRepoMeta.Statistics[imageMeta.Digest.String()]; !ok { protoRepoMeta.Statistics[imageMeta.Digest.String()] = &proto_go.DescriptorStatistics{ DownloadCount: 0, LastPullTimestamp: ×tamppb.Timestamp{}, PushTimestamp: timestamppb.Now(), PushedBy: userid, } } else if protoRepoMeta.Statistics[imageMeta.Digest.String()].PushTimestamp.AsTime().IsZero() { protoRepoMeta.Statistics[imageMeta.Digest.String()].PushTimestamp = timestamppb.Now() } if _, ok := protoRepoMeta.Signatures[imageMeta.Digest.String()]; !ok { protoRepoMeta.Signatures[imageMeta.Digest.String()] = &proto_go.ManifestSignatures{ Map: map[string]*proto_go.SignaturesInfo{"": {}}, } } if _, ok := protoRepoMeta.Referrers[imageMeta.Digest.String()]; !ok { protoRepoMeta.Referrers[imageMeta.Digest.String()] = &proto_go.ReferrersInfo{ List: []*proto_go.ReferrerInfo{}, } } // 4. Blobs repoBlobsBytes := repoBlobsBuck.Get([]byte(repo)) repoBlobs, err := unmarshalProtoRepoBlobs(repo, repoBlobsBytes) if err != nil { return err } protoRepoMeta, repoBlobs = common.AddImageMetaToRepoMeta(protoRepoMeta, repoBlobs, reference, imageMeta) err = setRepoLastUpdated(repo, time.Now(), repoLastUpdatedBuck) if err != nil { return err } err = setProtoRepoBlobs(repoBlobs, repoBlobsBuck) if err != nil { return err } return setProtoRepoMeta(protoRepoMeta, repoBuck) }) return err } func setRepoLastUpdated(repo string, lastUpdated time.Time, repoLastUpdatedBuck *bbolt.Bucket) error { protoTime := timestamppb.New(lastUpdated) protoTimeBlob, err := proto.Marshal(protoTime) if err != nil { return err } return repoLastUpdatedBuck.Put([]byte(repo), protoTimeBlob) } func unmarshalProtoRepoBlobs(repo string, repoBlobsBytes []byte) (*proto_go.RepoBlobs, error) { repoBlobs := &proto_go.RepoBlobs{ Name: repo, } if len(repoBlobsBytes) > 0 { err := proto.Unmarshal(repoBlobsBytes, repoBlobs) if err != nil { return nil, err } } if repoBlobs.Blobs == nil { repoBlobs.Blobs = map[string]*proto_go.BlobInfo{"": {}} } return repoBlobs, nil } func setProtoRepoBlobs(repoBlobs *proto_go.RepoBlobs, repoBlobsBuck *bbolt.Bucket) error { repoBlobsBytes, err := proto.Marshal(repoBlobs) if err != nil { return err } return repoBlobsBuck.Put([]byte(repoBlobs.Name), repoBlobsBytes) } func getProtoRepoMeta(repo string, repoMetaBuck *bbolt.Bucket) (*proto_go.RepoMeta, error) { repoMetaBlob := repoMetaBuck.Get([]byte(repo)) return unmarshalProtoRepoMeta(repo, repoMetaBlob) } // unmarshalProtoRepoMeta will unmarshal the repoMeta blob and initialize nil maps. If the blob is empty // an empty initialized object is returned. func unmarshalProtoRepoMeta(repo string, repoMetaBlob []byte) (*proto_go.RepoMeta, error) { protoRepoMeta := &proto_go.RepoMeta{ Name: repo, } if len(repoMetaBlob) > 0 { err := proto.Unmarshal(repoMetaBlob, protoRepoMeta) if err != nil { return protoRepoMeta, err } } if protoRepoMeta.Tags == nil { protoRepoMeta.Tags = map[string]*proto_go.TagDescriptor{"": {}} } if protoRepoMeta.Statistics == nil { protoRepoMeta.Statistics = map[string]*proto_go.DescriptorStatistics{"": {}} } if protoRepoMeta.Signatures == nil { protoRepoMeta.Signatures = map[string]*proto_go.ManifestSignatures{"": {}} } if protoRepoMeta.Referrers == nil { protoRepoMeta.Referrers = map[string]*proto_go.ReferrersInfo{"": {}} } if len(repoMetaBlob) == 0 { return protoRepoMeta, zerr.ErrRepoMetaNotFound } return protoRepoMeta, nil } func setProtoRepoMeta(repoMeta *proto_go.RepoMeta, repoBuck *bbolt.Bucket) error { repoMetaBlob, err := proto.Marshal(repoMeta) if err != nil { return err } return repoBuck.Put([]byte(repoMeta.Name), repoMetaBlob) } func (bdw *BoltDB) FilterImageMeta(ctx context.Context, digests []string, ) (map[string]mTypes.ImageMeta, error) { imageMetaMap := map[string]mTypes.ImageMeta{} err := bdw.DB.View(func(transaction *bbolt.Tx) error { imageBuck := transaction.Bucket([]byte(ImageMetaBuck)) for _, digest := range digests { protoImageMeta, err := getProtoImageMeta(imageBuck, digest) if err != nil { return err } if protoImageMeta.MediaType == ispec.MediaTypeImageIndex { manifestDataList := make([]*proto_go.ManifestMeta, 0, len(protoImageMeta.Index.Index.Manifests)) for _, manifest := range protoImageMeta.Index.Index.Manifests { imageManifestData, err := getProtoImageMeta(imageBuck, manifest.Digest) if err != nil { return err } manifestDataList = append(manifestDataList, imageManifestData.Manifests[0]) } protoImageMeta.Manifests = manifestDataList } imageMetaMap[digest] = mConvert.GetImageMeta(protoImageMeta) } return nil }) return imageMetaMap, err } func (bdw *BoltDB) SearchRepos(ctx context.Context, searchText string, ) ([]mTypes.RepoMeta, error) { repos := []mTypes.RepoMeta{} err := bdw.DB.View(func(transaction *bbolt.Tx) error { var ( repoBuck = transaction.Bucket([]byte(RepoMetaBuck)) userBookmarks = getUserBookmarks(ctx, transaction) userStars = getUserStars(ctx, transaction) ) cursor := repoBuck.Cursor() for repoName, repoMetaBlob := cursor.First(); repoName != nil; repoName, repoMetaBlob = cursor.Next() { if ok, err := reqCtx.RepoIsUserAvailable(ctx, string(repoName)); !ok || err != nil { continue } rank := common.RankRepoName(searchText, string(repoName)) if rank == -1 { continue } protoRepoMeta, err := unmarshalProtoRepoMeta(string(repoName), repoMetaBlob) if err != nil { return err } delete(protoRepoMeta.Tags, "") if len(protoRepoMeta.Tags) == 0 { continue } protoRepoMeta.Rank = int32(rank) protoRepoMeta.IsStarred = zcommon.Contains(userStars, protoRepoMeta.Name) protoRepoMeta.IsBookmarked = zcommon.Contains(userBookmarks, protoRepoMeta.Name) repos = append(repos, mConvert.GetRepoMeta(protoRepoMeta)) } return nil }) return repos, err } func getProtoImageMeta(imageBuck *bbolt.Bucket, digest string) (*proto_go.ImageMeta, error) { imageMetaBlob := imageBuck.Get([]byte(digest)) if len(imageMetaBlob) == 0 { return nil, zerr.ErrImageMetaNotFound } imageMeta := proto_go.ImageMeta{} err := proto.Unmarshal(imageMetaBlob, &imageMeta) if err != nil { return nil, err } return &imageMeta, nil } func (bdw *BoltDB) SearchTags(ctx context.Context, searchText string, ) ([]mTypes.FullImageMeta, error) { images := []mTypes.FullImageMeta{} searchedRepo, searchedTag, err := common.GetRepoTag(searchText) if err != nil { return []mTypes.FullImageMeta{}, fmt.Errorf("failed to parse search text, invalid format %w", err) } err = bdw.DB.View(func(transaction *bbolt.Tx) error { var ( repoBuck = transaction.Bucket([]byte(RepoMetaBuck)) imageBuck = transaction.Bucket([]byte(ImageMetaBuck)) userBookmarks = getUserBookmarks(ctx, transaction) userStars = getUserStars(ctx, transaction) ) repoName, repoMetaBlob := repoBuck.Cursor().Seek([]byte(searchedRepo)) if string(repoName) != searchedRepo { return nil } if ok, err := reqCtx.RepoIsUserAvailable(ctx, string(repoName)); !ok || err != nil { return err } protoRepoMeta, err := unmarshalProtoRepoMeta(string(repoName), repoMetaBlob) if err != nil { return err } delete(protoRepoMeta.Tags, "") protoRepoMeta.IsBookmarked = zcommon.Contains(userBookmarks, protoRepoMeta.Name) protoRepoMeta.IsStarred = zcommon.Contains(userStars, protoRepoMeta.Name) for tag, descriptor := range protoRepoMeta.Tags { if !strings.HasPrefix(tag, searchedTag) || tag == "" { continue } var protoImageMeta *proto_go.ImageMeta switch descriptor.MediaType { case ispec.MediaTypeImageManifest: manifestDigest := descriptor.Digest imageManifestData, err := getProtoImageMeta(imageBuck, manifestDigest) if err != nil { return fmt.Errorf("failed to fetch manifest meta for manifest with digest %s %w", manifestDigest, err) } protoImageMeta = imageManifestData case ispec.MediaTypeImageIndex: indexDigest := descriptor.Digest imageIndexData, err := getProtoImageMeta(imageBuck, indexDigest) if err != nil { return fmt.Errorf("failed to fetch manifest meta for manifest with digest %s %w", indexDigest, err) } manifestDataList := make([]*proto_go.ManifestMeta, 0, len(imageIndexData.Index.Index.Manifests)) for _, manifest := range imageIndexData.Index.Index.Manifests { imageManifestData, err := getProtoImageMeta(imageBuck, manifest.Digest) if err != nil { return err } manifestDataList = append(manifestDataList, imageManifestData.Manifests[0]) } imageIndexData.Manifests = manifestDataList protoImageMeta = imageIndexData default: bdw.Log.Error().Str("mediaType", descriptor.MediaType).Msg("unsupported media type") continue } images = append(images, mConvert.GetFullImageMetaFromProto(tag, protoRepoMeta, protoImageMeta)) } return nil }) return images, err } func (bdw *BoltDB) FilterTags(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { images := []mTypes.FullImageMeta{} err := bdw.DB.View(func(transaction *bbolt.Tx) error { var ( repoBuck = transaction.Bucket([]byte(RepoMetaBuck)) imageMetaBuck = transaction.Bucket([]byte(ImageMetaBuck)) userBookmarks = getUserBookmarks(ctx, transaction) userStars = getUserStars(ctx, transaction) viewError error ) cursor := repoBuck.Cursor() repoName, repoMetaBlob := cursor.First() for ; repoName != nil; repoName, repoMetaBlob = cursor.Next() { if ok, err := reqCtx.RepoIsUserAvailable(ctx, string(repoName)); !ok || err != nil { continue } protoRepoMeta, err := unmarshalProtoRepoMeta(string(repoName), repoMetaBlob) if err != nil { viewError = errors.Join(viewError, err) continue } delete(protoRepoMeta.Tags, "") protoRepoMeta.IsBookmarked = zcommon.Contains(userBookmarks, protoRepoMeta.Name) protoRepoMeta.IsStarred = zcommon.Contains(userStars, protoRepoMeta.Name) repoMeta := mConvert.GetRepoMeta(protoRepoMeta) for tag, descriptor := range protoRepoMeta.Tags { if !filterRepoTag(string(repoName), tag) { continue } switch descriptor.MediaType { case ispec.MediaTypeImageManifest: manifestDigest := descriptor.Digest imageManifestData, err := getProtoImageMeta(imageMetaBuck, manifestDigest) if err != nil { viewError = errors.Join(viewError, err) continue } imageMeta := mConvert.GetImageMeta(imageManifestData) if filterFunc(repoMeta, imageMeta) { images = append(images, mConvert.GetFullImageMetaFromProto(tag, protoRepoMeta, imageManifestData)) } case ispec.MediaTypeImageIndex: indexDigest := descriptor.Digest protoImageIndexMeta, err := getProtoImageMeta(imageMetaBuck, indexDigest) if err != nil { viewError = errors.Join(viewError, err) continue } imageIndexMeta := mConvert.GetImageMeta(protoImageIndexMeta) matchedManifests := []*proto_go.ManifestMeta{} for _, manifest := range protoImageIndexMeta.Index.Index.Manifests { manifestDigest := manifest.Digest imageManifestData, err := getProtoImageMeta(imageMetaBuck, manifestDigest) if err != nil { viewError = errors.Join(viewError, err) continue } imageMeta := mConvert.GetImageMeta(imageManifestData) partialImageMeta := common.GetPartialImageMeta(imageIndexMeta, imageMeta) if filterFunc(repoMeta, partialImageMeta) { matchedManifests = append(matchedManifests, imageManifestData.Manifests[0]) } } if len(matchedManifests) > 0 { protoImageIndexMeta.Manifests = matchedManifests images = append(images, mConvert.GetFullImageMetaFromProto(tag, protoRepoMeta, protoImageIndexMeta)) } default: bdw.Log.Error().Str("mediaType", descriptor.MediaType).Msg("unsupported media type") continue } } } return viewError }) return images, err } func (bdw *BoltDB) FilterRepos(ctx context.Context, acceptName mTypes.FilterRepoNameFunc, filter mTypes.FilterFullRepoFunc, ) ([]mTypes.RepoMeta, error) { repos := []mTypes.RepoMeta{} err := bdw.DB.View(func(tx *bbolt.Tx) error { var ( buck = tx.Bucket([]byte(RepoMetaBuck)) cursor = buck.Cursor() userBookmarks = getUserBookmarks(ctx, tx) userStars = getUserStars(ctx, tx) ) for repoName, repoMetaBlob := cursor.First(); repoName != nil; repoName, repoMetaBlob = cursor.Next() { if ok, err := reqCtx.RepoIsUserAvailable(ctx, string(repoName)); !ok || err != nil { continue } if !acceptName(string(repoName)) { continue } repoMeta, err := unmarshalProtoRepoMeta(string(repoName), repoMetaBlob) if err != nil { return err } repoMeta.IsBookmarked = zcommon.Contains(userBookmarks, repoMeta.Name) repoMeta.IsStarred = zcommon.Contains(userStars, repoMeta.Name) fullRepoMeta := mConvert.GetRepoMeta(repoMeta) if filter(fullRepoMeta) { repos = append(repos, fullRepoMeta) } } return nil }) if err != nil { return []mTypes.RepoMeta{}, err } return repos, err } func (bdw *BoltDB) GetRepoMeta(ctx context.Context, repo string) (mTypes.RepoMeta, error) { var protoRepoMeta *proto_go.RepoMeta err := bdw.DB.View(func(tx *bbolt.Tx) error { buck := tx.Bucket([]byte(RepoMetaBuck)) userBookmarks := getUserBookmarks(ctx, tx) userStars := getUserStars(ctx, tx) repoMetaBlob := buck.Get([]byte(repo)) var err error protoRepoMeta, err = unmarshalProtoRepoMeta(repo, repoMetaBlob) if err != nil { return err } delete(protoRepoMeta.Tags, "") protoRepoMeta.IsBookmarked = zcommon.Contains(userBookmarks, repo) protoRepoMeta.IsStarred = zcommon.Contains(userStars, repo) return nil }) return mConvert.GetRepoMeta(protoRepoMeta), err } func (bdw *BoltDB) GetFullImageMeta(ctx context.Context, repo string, tag string) (mTypes.FullImageMeta, error) { protoRepoMeta := &proto_go.RepoMeta{} protoImageMeta := &proto_go.ImageMeta{} err := bdw.DB.View(func(tx *bbolt.Tx) error { buck := tx.Bucket([]byte(RepoMetaBuck)) imageBuck := tx.Bucket([]byte(ImageMetaBuck)) userBookmarks := getUserBookmarks(ctx, tx) userStars := getUserStars(ctx, tx) repoMetaBlob := buck.Get([]byte(repo)) // object not found if len(repoMetaBlob) == 0 { return zerr.ErrRepoMetaNotFound } var err error protoRepoMeta, err = unmarshalProtoRepoMeta(repo, repoMetaBlob) if err != nil { return err } delete(protoRepoMeta.Tags, "") protoRepoMeta.IsBookmarked = zcommon.Contains(userBookmarks, repo) protoRepoMeta.IsStarred = zcommon.Contains(userStars, repo) descriptor, ok := protoRepoMeta.Tags[tag] if !ok { return zerr.ErrImageMetaNotFound } protoImageMeta, err = getProtoImageMeta(imageBuck, descriptor.Digest) if err != nil { return err } if protoImageMeta.MediaType == ispec.MediaTypeImageIndex { manifestDataList := make([]*proto_go.ManifestMeta, 0, len(protoImageMeta.Index.Index.Manifests)) for _, manifest := range protoImageMeta.Index.Index.Manifests { imageManifestData, err := getProtoImageMeta(imageBuck, manifest.Digest) if err != nil { return err } manifestDataList = append(manifestDataList, imageManifestData.Manifests[0]) } protoImageMeta.Manifests = manifestDataList } return nil }) return mConvert.GetFullImageMetaFromProto(tag, protoRepoMeta, protoImageMeta), err } func (bdw *BoltDB) GetImageMeta(digest godigest.Digest) (mTypes.ImageMeta, error) { imageMeta := mTypes.ImageMeta{} err := bdw.DB.View(func(tx *bbolt.Tx) error { imageBuck := tx.Bucket([]byte(ImageMetaBuck)) protoImageMeta, err := getProtoImageMeta(imageBuck, digest.String()) if err != nil { return err } if protoImageMeta.MediaType == ispec.MediaTypeImageIndex { manifestDataList := make([]*proto_go.ManifestMeta, 0, len(protoImageMeta.Index.Index.Manifests)) for _, manifest := range protoImageMeta.Index.Index.Manifests { imageManifestData, err := getProtoImageMeta(imageBuck, manifest.Digest) if err != nil { return err } manifestDataList = append(manifestDataList, imageManifestData.Manifests[0]) } protoImageMeta.Manifests = manifestDataList } imageMeta = mConvert.GetImageMeta(protoImageMeta) return nil }) return imageMeta, err } func (bdw *BoltDB) GetMultipleRepoMeta(ctx context.Context, filter func(repoMeta mTypes.RepoMeta) bool, ) ([]mTypes.RepoMeta, error) { foundRepos := []mTypes.RepoMeta{} err := bdw.DB.View(func(tx *bbolt.Tx) error { buck := tx.Bucket([]byte(RepoMetaBuck)) cursor := buck.Cursor() for repoName, repoMetaBlob := cursor.First(); repoName != nil; repoName, repoMetaBlob = cursor.Next() { if ok, err := reqCtx.RepoIsUserAvailable(ctx, string(repoName)); !ok || err != nil { continue } protoRepoMeta, err := unmarshalProtoRepoMeta(string(repoName), repoMetaBlob) if err != nil { return err } delete(protoRepoMeta.Tags, "") repoMeta := mConvert.GetRepoMeta(protoRepoMeta) if filter(repoMeta) { foundRepos = append(foundRepos, repoMeta) } } return nil }) return foundRepos, err } func (bdw *BoltDB) AddManifestSignature(repo string, signedManifestDigest godigest.Digest, sigMeta mTypes.SignatureMetadata, ) error { err := bdw.DB.Update(func(tx *bbolt.Tx) error { repoMetaBuck := tx.Bucket([]byte(RepoMetaBuck)) repoMetaBlob := repoMetaBuck.Get([]byte(repo)) if len(repoMetaBlob) == 0 { var err error // create a new object repoMeta := proto_go.RepoMeta{ Name: repo, Tags: map[string]*proto_go.TagDescriptor{"": {}}, Signatures: map[string]*proto_go.ManifestSignatures{ signedManifestDigest.String(): { Map: map[string]*proto_go.SignaturesInfo{ sigMeta.SignatureType: { List: []*proto_go.SignatureInfo{ { SignatureManifestDigest: sigMeta.SignatureDigest, LayersInfo: mConvert.GetProtoLayersInfo(sigMeta.LayersInfo), }, }, }, }, }, }, Referrers: map[string]*proto_go.ReferrersInfo{"": {}}, Statistics: map[string]*proto_go.DescriptorStatistics{"": {}}, } repoMetaBlob, err = proto.Marshal(&repoMeta) if err != nil { return err } return repoMetaBuck.Put([]byte(repo), repoMetaBlob) } protoRepoMeta, err := unmarshalProtoRepoMeta(repo, repoMetaBlob) if err != nil { return err } var ( manifestSignatures *proto_go.ManifestSignatures found bool ) if manifestSignatures, found = protoRepoMeta.Signatures[signedManifestDigest.String()]; !found { manifestSignatures = &proto_go.ManifestSignatures{Map: map[string]*proto_go.SignaturesInfo{"": {}}} } signatureSlice := &proto_go.SignaturesInfo{List: []*proto_go.SignatureInfo{}} if sigSlice, found := manifestSignatures.Map[sigMeta.SignatureType]; found { signatureSlice = sigSlice } if !common.ProtoSignatureAlreadyExists(signatureSlice.List, sigMeta) { switch sigMeta.SignatureType { case zcommon.NotationSignature: signatureSlice.List = append(signatureSlice.List, &proto_go.SignatureInfo{ SignatureManifestDigest: sigMeta.SignatureDigest, LayersInfo: mConvert.GetProtoLayersInfo(sigMeta.LayersInfo), }) case zcommon.CosignSignature: newCosignSig := &proto_go.SignatureInfo{ SignatureManifestDigest: sigMeta.SignatureDigest, LayersInfo: mConvert.GetProtoLayersInfo(sigMeta.LayersInfo), } if zcommon.IsCosignTag(sigMeta.SignatureTag) { // the entry for "sha256-{digest}.sig" signatures should be overwritten if // it exists or added on the first position if it doesn't exist if len(signatureSlice.GetList()) == 0 { signatureSlice.List = []*proto_go.SignatureInfo{newCosignSig} } else { signatureSlice.List[0] = newCosignSig } } else { // the first position should be reserved for "sha256-{digest}.sig" signatures if len(signatureSlice.GetList()) == 0 { signatureSlice.List = []*proto_go.SignatureInfo{{ SignatureManifestDigest: "", LayersInfo: []*proto_go.LayersInfo{}, }} } signatureSlice.List = append(signatureSlice.List, newCosignSig) } } } manifestSignatures.Map[sigMeta.SignatureType] = signatureSlice protoRepoMeta.Signatures[signedManifestDigest.String()] = manifestSignatures return setProtoRepoMeta(protoRepoMeta, repoMetaBuck) }) return err } func (bdw *BoltDB) DeleteSignature(repo string, signedManifestDigest godigest.Digest, sigMeta mTypes.SignatureMetadata, ) error { err := bdw.DB.Update(func(tx *bbolt.Tx) error { repoMetaBuck := tx.Bucket([]byte(RepoMetaBuck)) repoMetaBlob := repoMetaBuck.Get([]byte(repo)) if len(repoMetaBlob) == 0 { return zerr.ErrImageMetaNotFound } protoRepoMeta, err := unmarshalProtoRepoMeta(repo, repoMetaBlob) if err != nil { return err } manifestSignatures, found := protoRepoMeta.Signatures[signedManifestDigest.String()] if !found { return zerr.ErrImageMetaNotFound } signatureSlice := manifestSignatures.Map[sigMeta.SignatureType] newSignatureSlice := make([]*proto_go.SignatureInfo, 0, len(signatureSlice.List)) for _, sigInfo := range signatureSlice.List { if sigInfo.SignatureManifestDigest != sigMeta.SignatureDigest { newSignatureSlice = append(newSignatureSlice, sigInfo) } } manifestSignatures.Map[sigMeta.SignatureType] = &proto_go.SignaturesInfo{List: newSignatureSlice} protoRepoMeta.Signatures[signedManifestDigest.String()] = manifestSignatures return setProtoRepoMeta(protoRepoMeta, repoMetaBuck) }) return err } func (bdw *BoltDB) IncrementRepoStars(repo string) error { err := bdw.DB.Update(func(tx *bbolt.Tx) error { repoMetaBuck := tx.Bucket([]byte(RepoMetaBuck)) repoMetaBlob := repoMetaBuck.Get([]byte(repo)) if len(repoMetaBlob) == 0 { return zerr.ErrRepoMetaNotFound } protoRepoMeta, err := unmarshalProtoRepoMeta(repo, repoMetaBlob) if err != nil { return err } protoRepoMeta.Stars++ return setProtoRepoMeta(protoRepoMeta, repoMetaBuck) }) return err } func (bdw *BoltDB) DecrementRepoStars(repo string) error { err := bdw.DB.Update(func(tx *bbolt.Tx) error { repoMetaBuck := tx.Bucket([]byte(RepoMetaBuck)) repoMetaBlob := repoMetaBuck.Get([]byte(repo)) if len(repoMetaBlob) == 0 { return zerr.ErrRepoMetaNotFound } protoRepoMeta, err := unmarshalProtoRepoMeta(repo, repoMetaBlob) if err != nil { return err } if protoRepoMeta.Stars == 0 { return nil } protoRepoMeta.Stars-- return setProtoRepoMeta(protoRepoMeta, repoMetaBuck) }) return err } func (bdw *BoltDB) SetRepoMeta(repo string, repoMeta mTypes.RepoMeta) error { err := bdw.DB.Update(func(tx *bbolt.Tx) error { buck := tx.Bucket([]byte(RepoMetaBuck)) repoLastUpdatedBuck := tx.Bucket([]byte(RepoBlobsBuck)).Bucket([]byte(RepoLastUpdatedBuck)) repoMeta.Name = repo repoMetaBlob, err := proto.Marshal(mConvert.GetProtoRepoMeta(repoMeta)) if err != nil { return err } err = buck.Put([]byte(repo), repoMetaBlob) if err != nil { return err } // The last update time is set to 0 in order to force an update in case of a next storage parsing return setRepoLastUpdated(repo, time.Time{}, repoLastUpdatedBuck) }) return err } func (bdw *BoltDB) DeleteRepoMeta(repo string) error { err := bdw.DB.Update(func(tx *bbolt.Tx) error { repoBuck := tx.Bucket([]byte(RepoMetaBuck)) repoBlobsBuck := tx.Bucket([]byte(RepoBlobsBuck)) repoLastUpdatedBuck := repoBlobsBuck.Bucket([]byte(RepoLastUpdatedBuck)) err := repoBuck.Delete([]byte(repo)) if err != nil { return err } err = repoBlobsBuck.Delete([]byte(repo)) if err != nil { return err } return repoLastUpdatedBuck.Delete([]byte(repo)) }) return err } func (bdw *BoltDB) ResetRepoReferences(repo string) error { err := bdw.DB.Update(func(tx *bbolt.Tx) error { buck := tx.Bucket([]byte(RepoMetaBuck)) repoMetaBlob := buck.Get([]byte(repo)) protoRepoMeta, err := unmarshalProtoRepoMeta(repo, repoMetaBlob) if err != nil && !errors.Is(err, zerr.ErrRepoMetaNotFound) { return err } repoMetaBlob, err = proto.Marshal(&proto_go.RepoMeta{ Name: repo, Statistics: protoRepoMeta.Statistics, Stars: protoRepoMeta.Stars, Tags: map[string]*proto_go.TagDescriptor{"": {}}, Signatures: map[string]*proto_go.ManifestSignatures{"": {Map: map[string]*proto_go.SignaturesInfo{"": {}}}}, Referrers: map[string]*proto_go.ReferrersInfo{"": {}}, }) if err != nil { return err } return buck.Put([]byte(repo), repoMetaBlob) }) return err } func (bdw *BoltDB) GetReferrersInfo(repo string, referredDigest godigest.Digest, artifactTypes []string, ) ([]mTypes.ReferrerInfo, error) { referrersInfoResult := []mTypes.ReferrerInfo{} err := bdw.DB.View(func(tx *bbolt.Tx) error { buck := tx.Bucket([]byte(RepoMetaBuck)) repoMetaBlob := buck.Get([]byte(repo)) protoRepoMeta, err := unmarshalProtoRepoMeta(repo, repoMetaBlob) if err != nil { return err } referrersInfo := protoRepoMeta.Referrers[referredDigest.String()].List for i := range referrersInfo { if !common.MatchesArtifactTypes(referrersInfo[i].ArtifactType, artifactTypes) { continue } referrersInfoResult = append(referrersInfoResult, mTypes.ReferrerInfo{ Digest: referrersInfo[i].Digest, MediaType: referrersInfo[i].MediaType, ArtifactType: referrersInfo[i].ArtifactType, Size: int(referrersInfo[i].Size), Annotations: referrersInfo[i].Annotations, }) } return nil }) return referrersInfoResult, err } func (bdw *BoltDB) UpdateStatsOnDownload(repo string, reference string) error { err := bdw.DB.Update(func(tx *bbolt.Tx) error { repoMetaBuck := tx.Bucket([]byte(RepoMetaBuck)) repoMetaBlob := repoMetaBuck.Get([]byte(repo)) if len(repoMetaBlob) == 0 { return zerr.ErrRepoMetaNotFound } protoRepoMeta, err := unmarshalProtoRepoMeta(repo, repoMetaBlob) if err != nil { return err } manifestDigest := reference if common.ReferenceIsTag(reference) { descriptor, found := protoRepoMeta.Tags[reference] if !found { return zerr.ErrImageMetaNotFound } manifestDigest = descriptor.Digest } manifestStatistics, ok := protoRepoMeta.Statistics[manifestDigest] if !ok { return zerr.ErrImageMetaNotFound } manifestStatistics.DownloadCount++ manifestStatistics.LastPullTimestamp = timestamppb.Now() protoRepoMeta.Statistics[manifestDigest] = manifestStatistics return setProtoRepoMeta(protoRepoMeta, repoMetaBuck) }) return err } func (bdw *BoltDB) UpdateSignaturesValidity(ctx context.Context, repo string, manifestDigest godigest.Digest) error { err := bdw.DB.Update(func(transaction *bbolt.Tx) error { imgTrustStore := bdw.ImageTrustStore() if imgTrustStore == nil { return nil } // get ManifestData of signed manifest imageMetaBuck := transaction.Bucket([]byte(ImageMetaBuck)) idBlob := imageMetaBuck.Get([]byte(manifestDigest)) if len(idBlob) == 0 { // manifest meta not found, updating signatures with details about validity and author will not be performed return nil } protoImageMeta := proto_go.ImageMeta{} err := proto.Unmarshal(idBlob, &protoImageMeta) if err != nil { return err } // update signatures with details about validity and author repoBuck := transaction.Bucket([]byte(RepoMetaBuck)) repoMetaBlob := repoBuck.Get([]byte(repo)) if len(repoMetaBlob) == 0 { return zerr.ErrRepoMetaNotFound } protoRepoMeta, err := unmarshalProtoRepoMeta(repo, repoMetaBlob) if err != nil { return err } manifestSignatures := proto_go.ManifestSignatures{Map: map[string]*proto_go.SignaturesInfo{"": {}}} for sigType, sigs := range protoRepoMeta.Signatures[manifestDigest.String()].Map { if zcommon.IsContextDone(ctx) { return ctx.Err() } signaturesInfo := []*proto_go.SignatureInfo{} for _, sigInfo := range sigs.List { layersInfo := []*proto_go.LayersInfo{} for _, layerInfo := range sigInfo.LayersInfo { author, date, isTrusted, _ := imgTrustStore.VerifySignature(sigType, layerInfo.LayerContent, layerInfo.SignatureKey, manifestDigest, mConvert.GetImageMeta(&protoImageMeta), repo) if isTrusted { layerInfo.Signer = author } if !date.IsZero() { layerInfo.Signer = author layerInfo.Date = timestamppb.New(date) } layersInfo = append(layersInfo, layerInfo) } signaturesInfo = append(signaturesInfo, &proto_go.SignatureInfo{ SignatureManifestDigest: sigInfo.SignatureManifestDigest, LayersInfo: layersInfo, }) } manifestSignatures.Map[sigType] = &proto_go.SignaturesInfo{List: signaturesInfo} } protoRepoMeta.Signatures[manifestDigest.String()] = &manifestSignatures return setProtoRepoMeta(protoRepoMeta, repoBuck) }) return err } func (bdw *BoltDB) RemoveRepoReference(repo, reference string, manifestDigest godigest.Digest) error { err := bdw.DB.Update(func(tx *bbolt.Tx) error { repoMetaBuck := tx.Bucket([]byte(RepoMetaBuck)) imageMetaBuck := tx.Bucket([]byte(ImageMetaBuck)) repoBlobsBuck := tx.Bucket([]byte(RepoBlobsBuck)) repoLastUpdatedBuck := repoBlobsBuck.Bucket([]byte(RepoLastUpdatedBuck)) protoRepoMeta, err := getProtoRepoMeta(repo, repoMetaBuck) if err != nil { if errors.Is(err, zerr.ErrRepoMetaNotFound) { return nil } return err } protoImageMeta, err := getProtoImageMeta(imageMetaBuck, manifestDigest.String()) if err != nil { if errors.Is(err, zerr.ErrImageMetaNotFound) { return nil } return err } // Remove Referrers if subject := mConvert.GetImageSubject(protoImageMeta); subject != nil { referredDigest := subject.Digest.String() refInfo := &proto_go.ReferrersInfo{} if protoRepoMeta.Referrers[referredDigest] != nil { refInfo = protoRepoMeta.Referrers[referredDigest] } referrers := refInfo.List for i := range referrers { if referrers[i].Digest == manifestDigest.String() { referrers[i].Count -= 1 if referrers[i].Count == 0 || common.ReferenceIsDigest(reference) { referrers = append(referrers[:i], referrers[i+1:]...) } break } } refInfo.List = referrers protoRepoMeta.Referrers[referredDigest] = refInfo } if !common.ReferenceIsDigest(reference) { delete(protoRepoMeta.Tags, reference) } else { // remove all tags pointing to this digest for tag, desc := range protoRepoMeta.Tags { if desc.Digest == reference { delete(protoRepoMeta.Tags, tag) } } } /* try to find at least one tag pointing to manifestDigest if not found then we can also remove everything related to this digest */ var foundTag bool for _, desc := range protoRepoMeta.Tags { if desc.Digest == manifestDigest.String() { foundTag = true } } if !foundTag { delete(protoRepoMeta.Statistics, manifestDigest.String()) delete(protoRepoMeta.Signatures, manifestDigest.String()) delete(protoRepoMeta.Referrers, manifestDigest.String()) } repoBlobsBytes := repoBlobsBuck.Get([]byte(protoRepoMeta.Name)) repoBlobs, err := unmarshalProtoRepoBlobs(repo, repoBlobsBytes) if err != nil { return err } err = setRepoLastUpdated(repo, time.Now(), repoLastUpdatedBuck) if err != nil { return err } protoRepoMeta, repoBlobs = common.RemoveImageFromRepoMeta(protoRepoMeta, repoBlobs, reference) repoBlobsBytes, err = proto.Marshal(repoBlobs) if err != nil { return err } err = repoBlobsBuck.Put([]byte(protoRepoMeta.Name), repoBlobsBytes) if err != nil { return err } return setProtoRepoMeta(protoRepoMeta, repoMetaBuck) }) return err } func (bdw *BoltDB) ImageTrustStore() mTypes.ImageTrustStore { return bdw.imgTrustStore } func (bdw *BoltDB) SetImageTrustStore(imgTrustStore mTypes.ImageTrustStore) { bdw.imgTrustStore = imgTrustStore } func (bdw *BoltDB) ToggleStarRepo(ctx context.Context, repo string) (mTypes.ToggleState, error) { userAc, err := reqCtx.UserAcFromContext(ctx) if err != nil { return mTypes.NotChanged, err } if userAc.IsAnonymous() || !userAc.Can(constants.ReadPermission, repo) { return mTypes.NotChanged, zerr.ErrUserDataNotAllowed } userid := userAc.GetUsername() var res mTypes.ToggleState if err := bdw.DB.Update(func(tx *bbolt.Tx) error { //nolint:varnamelen var userData mTypes.UserData err := bdw.getUserData(userid, tx, &userData) if err != nil && !errors.Is(err, zerr.ErrUserDataNotFound) { return err } isRepoStarred := zcommon.Contains(userData.StarredRepos, repo) if isRepoStarred { res = mTypes.Removed userData.StarredRepos = zcommon.RemoveFrom(userData.StarredRepos, repo) } else { res = mTypes.Added userData.StarredRepos = append(userData.StarredRepos, repo) } err = bdw.setUserData(userid, tx, userData) if err != nil { return err } repoBuck := tx.Bucket([]byte(RepoMetaBuck)) repoMetaBlob := repoBuck.Get([]byte(repo)) if len(repoMetaBlob) == 0 { return zerr.ErrRepoMetaNotFound } protoRepoMeta, err := unmarshalProtoRepoMeta(repo, repoMetaBlob) if err != nil { return err } switch res { case mTypes.Added: protoRepoMeta.Stars++ case mTypes.Removed: protoRepoMeta.Stars-- } return setProtoRepoMeta(protoRepoMeta, repoBuck) }); err != nil { return mTypes.NotChanged, err } return res, nil } func (bdw *BoltDB) GetStarredRepos(ctx context.Context) ([]string, error) { userData, err := bdw.GetUserData(ctx) if errors.Is(err, zerr.ErrUserDataNotFound) || errors.Is(err, zerr.ErrUserDataNotAllowed) { return []string{}, nil } return userData.StarredRepos, err } func (bdw *BoltDB) ToggleBookmarkRepo(ctx context.Context, repo string) (mTypes.ToggleState, error) { userAc, err := reqCtx.UserAcFromContext(ctx) if err != nil { return mTypes.NotChanged, err } if userAc.IsAnonymous() || !userAc.Can(constants.ReadPermission, repo) { return mTypes.NotChanged, zerr.ErrUserDataNotAllowed } userid := userAc.GetUsername() var res mTypes.ToggleState if err := bdw.DB.Update(func(transaction *bbolt.Tx) error { //nolint:dupl var userData mTypes.UserData err := bdw.getUserData(userid, transaction, &userData) if err != nil && !errors.Is(err, zerr.ErrUserDataNotFound) { return err } isRepoBookmarked := zcommon.Contains(userData.BookmarkedRepos, repo) if isRepoBookmarked { res = mTypes.Removed userData.BookmarkedRepos = zcommon.RemoveFrom(userData.BookmarkedRepos, repo) } else { res = mTypes.Added userData.BookmarkedRepos = append(userData.BookmarkedRepos, repo) } return bdw.setUserData(userid, transaction, userData) }); err != nil { return mTypes.NotChanged, err } return res, nil } func (bdw *BoltDB) GetBookmarkedRepos(ctx context.Context) ([]string, error) { userData, err := bdw.GetUserData(ctx) if errors.Is(err, zerr.ErrUserDataNotFound) || errors.Is(err, zerr.ErrUserDataNotAllowed) { return []string{}, nil } return userData.BookmarkedRepos, err } func (bdw *BoltDB) PatchDB() error { var DBVersion string err := bdw.DB.View(func(tx *bbolt.Tx) error { versionBuck := tx.Bucket([]byte(VersionBucket)) DBVersion = string(versionBuck.Get([]byte(version.DBVersionKey))) return nil }) if err != nil { return fmt.Errorf("patching the database failed, can't read db version %w", err) } if version.GetVersionIndex(DBVersion) == -1 { return fmt.Errorf("DB has broken format, no version found %w", err) } for patchIndex, patch := range bdw.Patches { if patchIndex < version.GetVersionIndex(DBVersion) { continue } err := patch(bdw.DB) if err != nil { return err } } return nil } func getUserStars(ctx context.Context, transaction *bbolt.Tx) []string { userAc, err := reqCtx.UserAcFromContext(ctx) if err != nil { return []string{} } var ( userData mTypes.UserData userid = userAc.GetUsername() userdb = transaction.Bucket([]byte(UserDataBucket)) ) if userid == "" || userdb == nil { return []string{} } mdata := userdb.Get([]byte(userid)) if mdata == nil { return []string{} } if err := json.Unmarshal(mdata, &userData); err != nil { return []string{} } return userData.StarredRepos } func getUserBookmarks(ctx context.Context, transaction *bbolt.Tx) []string { userAc, err := reqCtx.UserAcFromContext(ctx) if err != nil { return []string{} } var ( userData mTypes.UserData userid = userAc.GetUsername() userdb = transaction.Bucket([]byte(UserDataBucket)) ) if userid == "" || userdb == nil { return []string{} } mdata := userdb.Get([]byte(userid)) if mdata == nil { return []string{} } if err := json.Unmarshal(mdata, &userData); err != nil { return []string{} } return userData.BookmarkedRepos } func (bdw *BoltDB) SetUserGroups(ctx context.Context, groups []string) error { userAc, err := reqCtx.UserAcFromContext(ctx) if err != nil { return err } if userAc.IsAnonymous() { return zerr.ErrUserDataNotAllowed } userid := userAc.GetUsername() err = bdw.DB.Update(func(tx *bbolt.Tx) error { //nolint:varnamelen var userData mTypes.UserData err := bdw.getUserData(userid, tx, &userData) if err != nil && !errors.Is(err, zerr.ErrUserDataNotFound) { return err } userData.Groups = append(userData.Groups, groups...) err = bdw.setUserData(userid, tx, userData) return err }) return err } func (bdw *BoltDB) GetUserGroups(ctx context.Context) ([]string, error) { userData, err := bdw.GetUserData(ctx) return userData.Groups, err } func (bdw *BoltDB) UpdateUserAPIKeyLastUsed(ctx context.Context, hashedKey string) error { userAc, err := reqCtx.UserAcFromContext(ctx) if err != nil { return err } if userAc.IsAnonymous() { return zerr.ErrUserDataNotAllowed } userid := userAc.GetUsername() err = bdw.DB.Update(func(tx *bbolt.Tx) error { //nolint:varnamelen var userData mTypes.UserData err := bdw.getUserData(userid, tx, &userData) if err != nil { return err } apiKeyDetails := userData.APIKeys[hashedKey] apiKeyDetails.LastUsed = time.Now() userData.APIKeys[hashedKey] = apiKeyDetails err = bdw.setUserData(userid, tx, userData) return err }) return err } func (bdw *BoltDB) IsAPIKeyExpired(ctx context.Context, hashedKey string) (bool, error) { userAc, err := reqCtx.UserAcFromContext(ctx) if err != nil { return false, err } if userAc.IsAnonymous() { return false, zerr.ErrUserDataNotAllowed } userid := userAc.GetUsername() var isExpired bool err = bdw.DB.Update(func(tx *bbolt.Tx) error { //nolint:varnamelen var userData mTypes.UserData err := bdw.getUserData(userid, tx, &userData) if err != nil { return err } apiKeyDetails := userData.APIKeys[hashedKey] if apiKeyDetails.IsExpired { isExpired = true return nil } // if expiresAt is not nil value if !apiKeyDetails.ExpirationDate.Equal(time.Time{}) && time.Now().After(apiKeyDetails.ExpirationDate) { isExpired = true apiKeyDetails.IsExpired = true } userData.APIKeys[hashedKey] = apiKeyDetails err = bdw.setUserData(userid, tx, userData) return err }) return isExpired, err } func (bdw *BoltDB) GetUserAPIKeys(ctx context.Context) ([]mTypes.APIKeyDetails, error) { apiKeys := make([]mTypes.APIKeyDetails, 0) userAc, err := reqCtx.UserAcFromContext(ctx) if err != nil { return nil, err } if userAc.IsAnonymous() { return nil, zerr.ErrUserDataNotAllowed } userid := userAc.GetUsername() err = bdw.DB.Update(func(transaction *bbolt.Tx) error { var userData mTypes.UserData err = bdw.getUserData(userid, transaction, &userData) if err != nil && !errors.Is(err, zerr.ErrUserDataNotFound) { return err } for hashedKey, apiKeyDetails := range userData.APIKeys { // if expiresAt is not nil value if !apiKeyDetails.ExpirationDate.Equal(time.Time{}) && time.Now().After(apiKeyDetails.ExpirationDate) { apiKeyDetails.IsExpired = true } userData.APIKeys[hashedKey] = apiKeyDetails err = bdw.setUserData(userid, transaction, userData) if err != nil { return err } apiKeys = append(apiKeys, apiKeyDetails) } return nil }) return apiKeys, err } func (bdw *BoltDB) AddUserAPIKey(ctx context.Context, hashedKey string, apiKeyDetails *mTypes.APIKeyDetails) error { userAc, err := reqCtx.UserAcFromContext(ctx) if err != nil { return err } if userAc.IsAnonymous() { return zerr.ErrUserDataNotAllowed } userid := userAc.GetUsername() err = bdw.DB.Update(func(transaction *bbolt.Tx) error { var userData mTypes.UserData apiKeysbuck := transaction.Bucket([]byte(UserAPIKeysBucket)) if apiKeysbuck == nil { return zerr.ErrBucketDoesNotExist } err := apiKeysbuck.Put([]byte(hashedKey), []byte(userid)) if err != nil { return fmt.Errorf("failed to set userData for identity %s %w", userid, err) } err = bdw.getUserData(userid, transaction, &userData) if err != nil && !errors.Is(err, zerr.ErrUserDataNotFound) { return err } if userData.APIKeys == nil { userData.APIKeys = make(map[string]mTypes.APIKeyDetails) } userData.APIKeys[hashedKey] = *apiKeyDetails err = bdw.setUserData(userid, transaction, userData) return err }) return err } func (bdw *BoltDB) DeleteUserAPIKey(ctx context.Context, keyID string) error { userAc, err := reqCtx.UserAcFromContext(ctx) if err != nil { return err } if userAc.IsAnonymous() { return zerr.ErrUserDataNotAllowed } userid := userAc.GetUsername() err = bdw.DB.Update(func(transaction *bbolt.Tx) error { var userData mTypes.UserData apiKeysbuck := transaction.Bucket([]byte(UserAPIKeysBucket)) if apiKeysbuck == nil { return zerr.ErrBucketDoesNotExist } err := bdw.getUserData(userid, transaction, &userData) if err != nil { return err } for hash, apiKeyDetails := range userData.APIKeys { if apiKeyDetails.UUID == keyID { delete(userData.APIKeys, hash) err := apiKeysbuck.Delete([]byte(hash)) if err != nil { return fmt.Errorf("failed to delete userAPIKey entry for hash %s %w", hash, err) } } } return bdw.setUserData(userid, transaction, userData) }) return err } func (bdw *BoltDB) GetUserAPIKeyInfo(hashedKey string) (string, error) { var userid string err := bdw.DB.View(func(tx *bbolt.Tx) error { buck := tx.Bucket([]byte(UserAPIKeysBucket)) if buck == nil { return zerr.ErrBucketDoesNotExist } uiBlob := buck.Get([]byte(hashedKey)) if len(uiBlob) == 0 { return zerr.ErrUserAPIKeyNotFound } userid = string(uiBlob) return nil }) return userid, err } func (bdw *BoltDB) GetUserData(ctx context.Context) (mTypes.UserData, error) { var userData mTypes.UserData userAc, err := reqCtx.UserAcFromContext(ctx) if err != nil { return userData, err } if userAc.IsAnonymous() { return userData, zerr.ErrUserDataNotAllowed } userid := userAc.GetUsername() err = bdw.DB.View(func(tx *bbolt.Tx) error { return bdw.getUserData(userid, tx, &userData) }) return userData, err } func (bdw *BoltDB) getUserData(userid string, transaction *bbolt.Tx, res *mTypes.UserData) error { buck := transaction.Bucket([]byte(UserDataBucket)) if buck == nil { return zerr.ErrBucketDoesNotExist } upBlob := buck.Get([]byte(userid)) if len(upBlob) == 0 { return zerr.ErrUserDataNotFound } err := json.Unmarshal(upBlob, res) if err != nil { return err } return nil } func (bdw *BoltDB) SetUserData(ctx context.Context, userData mTypes.UserData) error { userAc, err := reqCtx.UserAcFromContext(ctx) if err != nil { return err } if userAc.IsAnonymous() { return zerr.ErrUserDataNotAllowed } userid := userAc.GetUsername() err = bdw.DB.Update(func(tx *bbolt.Tx) error { return bdw.setUserData(userid, tx, userData) }) return err } func (bdw *BoltDB) setUserData(userid string, transaction *bbolt.Tx, userData mTypes.UserData) error { buck := transaction.Bucket([]byte(UserDataBucket)) if buck == nil { return zerr.ErrBucketDoesNotExist } upBlob, err := json.Marshal(userData) if err != nil { return err } err = buck.Put([]byte(userid), upBlob) if err != nil { return fmt.Errorf("failed to set userData for identity %s %w", userid, err) } return nil } func (bdw *BoltDB) DeleteUserData(ctx context.Context) error { userAc, err := reqCtx.UserAcFromContext(ctx) if err != nil { return err } if userAc.IsAnonymous() { return zerr.ErrUserDataNotAllowed } userid := userAc.GetUsername() err = bdw.DB.Update(func(tx *bbolt.Tx) error { buck := tx.Bucket([]byte(UserDataBucket)) if buck == nil { return zerr.ErrBucketDoesNotExist } err := buck.Delete([]byte(userid)) if err != nil { return fmt.Errorf("failed to delete userData for identity %s %w", userid, err) } return nil }) return err } func (bdw *BoltDB) ResetDB() error { err := bdw.DB.Update(func(transaction *bbolt.Tx) error { err := resetBucket(transaction, RepoMetaBuck) if err != nil { return err } err = resetBucket(transaction, ImageMetaBuck) if err != nil { return err } err = resetBucket(transaction, RepoBlobsBuck) if err != nil { return err } err = resetBucket(transaction, UserAPIKeysBucket) if err != nil { return err } err = resetBucket(transaction, UserDataBucket) if err != nil { return err } return nil }) return err } func resetBucket(transaction *bbolt.Tx, bucketName string) error { bucket := transaction.Bucket([]byte(bucketName)) if bucket == nil { return nil } // we need to create the sub buckets if they exits, we'll presume the sub-buckets are not nested more than 1 layer subBuckets := [][]byte{} err := bucket.ForEachBucket(func(bucketName []byte) error { subBuckets = append(subBuckets, bucketName) return nil }) if err != nil { return err } err = transaction.DeleteBucket([]byte(bucketName)) if err != nil { return err } bucket, err = transaction.CreateBucketIfNotExists([]byte(bucketName)) if err != nil { return err } for _, subBucket := range subBuckets { _, err := bucket.CreateBucketIfNotExists(subBucket) if err != nil { return err } } return err }