drone/registry/app/api/handler/generic/base.go

245 lines
8.3 KiB
Go

// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generic
import (
"context"
"fmt"
"net/http"
"regexp"
"strings"
usercontroller "github.com/harness/gitness/app/api/controller/user"
"github.com/harness/gitness/app/auth/authn"
"github.com/harness/gitness/app/auth/authz"
corestore "github.com/harness/gitness/app/store"
urlprovider "github.com/harness/gitness/app/url"
"github.com/harness/gitness/registry/app/api/controller/metadata"
"github.com/harness/gitness/registry/app/api/handler/utils"
artifact2 "github.com/harness/gitness/registry/app/api/openapi/contracts/artifact"
"github.com/harness/gitness/registry/app/dist_temp/errcode"
"github.com/harness/gitness/registry/app/pkg"
"github.com/harness/gitness/registry/app/pkg/commons"
"github.com/harness/gitness/registry/app/pkg/generic"
"github.com/rs/zerolog/log"
)
const (
packageNameRegex = `^[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]$`
versionRegex = `^[a-z0-9][a-z0-9.-]*[a-z0-9]$`
filenameRegex = `^[a-zA-Z0-9][a-zA-Z0-9._~@,/-]*[a-zA-Z0-9]$`
// Add other route types here.
)
func NewGenericArtifactHandler(
spaceStore corestore.SpaceStore, controller *generic.Controller, tokenStore corestore.TokenStore,
userCtrl *usercontroller.Controller, authenticator authn.Authenticator, urlProvider urlprovider.Provider,
authorizer authz.Authorizer,
) *Handler {
return &Handler{
Controller: controller,
SpaceStore: spaceStore,
TokenStore: tokenStore,
UserCtrl: userCtrl,
Authenticator: authenticator,
URLProvider: urlProvider,
Authorizer: authorizer,
}
}
type Handler struct {
Controller *generic.Controller
SpaceStore corestore.SpaceStore
TokenStore corestore.TokenStore
UserCtrl *usercontroller.Controller
Authenticator authn.Authenticator
URLProvider urlprovider.Provider
Authorizer authz.Authorizer
}
func (h *Handler) GetArtifactInfo(r *http.Request) (pkg.GenericArtifactInfo, errcode.Error) {
ctx := r.Context()
path := r.URL.Path
rootIdentifier, registryIdentifier, artifact, tag, fileName, description, err := ExtractPathVars(r)
if err != nil {
return pkg.GenericArtifactInfo{}, errcode.ErrCodeInvalidRequest.WithDetail(err)
}
if err := metadata.ValidateIdentifier(registryIdentifier); err != nil {
return pkg.GenericArtifactInfo{}, errcode.ErrCodeInvalidRequest.WithDetail(err)
}
if err := validatePackageVersionAndFileName(artifact, tag, fileName); err != nil {
return pkg.GenericArtifactInfo{}, errcode.ErrCodeInvalidRequest.WithDetail(err)
}
rootSpace, err := h.SpaceStore.FindByRefCaseInsensitive(ctx, rootIdentifier)
if err != nil {
log.Ctx(ctx).Error().Msgf("Root space not found: %s", rootIdentifier)
return pkg.GenericArtifactInfo{}, errcode.ErrCodeRootNotFound.WithDetail(err)
}
registry, err := h.Controller.DBStore.RegistryDao.GetByRootParentIDAndName(ctx, rootSpace.ID, registryIdentifier)
if err != nil {
log.Ctx(ctx).Error().Msgf(
"registry %s not found for root: %s. Reason: %s", registryIdentifier, rootSpace.Identifier, err,
)
return pkg.GenericArtifactInfo{}, errcode.ErrCodeRegNotFound.WithDetail(err)
}
if registry.PackageType != artifact2.PackageTypeGENERIC {
log.Ctx(ctx).Error().Msgf(
"registry %s is not a generic artifact registry for root: %s", registryIdentifier, rootSpace.Identifier,
)
return pkg.GenericArtifactInfo{}, errcode.ErrCodeInvalidRequest.WithDetail(fmt.Errorf("registry %s is"+
" not a generic artifact registry", registryIdentifier))
}
_, err = h.SpaceStore.Find(r.Context(), registry.ParentID)
if err != nil {
log.Ctx(ctx).Error().Msgf("Parent space not found: %d", registry.ParentID)
return pkg.GenericArtifactInfo{}, errcode.ErrCodeParentNotFound.WithDetail(err)
}
info := &pkg.GenericArtifactInfo{
ArtifactInfo: &pkg.ArtifactInfo{
BaseInfo: &pkg.BaseInfo{
RootIdentifier: rootIdentifier,
RootParentID: rootSpace.ID,
ParentID: registry.ParentID,
},
RegIdentifier: registryIdentifier,
Image: artifact,
},
RegistryID: registry.ID,
Version: tag,
FileName: fileName,
Description: description,
}
log.Ctx(ctx).Info().Msgf("Dispatch: URI: %s", path)
if commons.IsEmpty(rootSpace.Identifier) {
log.Ctx(ctx).Error().Msgf("ParentRef not found in context")
return pkg.GenericArtifactInfo{}, errcode.ErrCodeParentNotFound.WithDetail(err)
}
if commons.IsEmpty(registryIdentifier) {
log.Ctx(ctx).Warn().Msgf("registry not found in context")
return pkg.GenericArtifactInfo{}, errcode.ErrCodeRegNotFound.WithDetail(err)
}
if !commons.IsEmpty(info.Image) && !commons.IsEmpty(info.Version) && !commons.IsEmpty(info.FileName) {
flag, err2 := utils.IsPatternAllowed(registry.AllowedPattern, registry.BlockedPattern,
info.Image+":"+info.Version+":"+info.FileName)
if !flag || err2 != nil {
return pkg.GenericArtifactInfo{}, errcode.ErrCodeInvalidRequest.WithDetail(err2)
}
}
return *info, errcode.Error{}
}
// ExtractPathVars extracts registry,image, reference, digest and tag from the path
// Path format: /generic/:rootSpace/:registry/:image/:tag (for ex:
// /generic/myRootSpace/reg1/alpine/v1).
func ExtractPathVars(r *http.Request) (
rootIdentifier, registry, artifact,
tag, fileName string, description string, err error,
) {
path := r.URL.Path
// Ensure the path starts with "/generic/"
if !strings.HasPrefix(path, "/generic/") {
return "", "", "", "", "", "", fmt.Errorf("invalid path: must start with /generic/")
}
trimmedPath := strings.TrimPrefix(path, "/generic/")
firstSlashIndex := strings.Index(trimmedPath, "/")
if firstSlashIndex == -1 {
return "", "", "", "", "", "", fmt.Errorf("invalid path format: missing rootIdentifier or registry")
}
rootIdentifier = trimmedPath[:firstSlashIndex]
remainingPath := trimmedPath[firstSlashIndex+1:]
secondSlashIndex := strings.Index(remainingPath, "/")
if secondSlashIndex == -1 {
return "", "", "", "", "", "", fmt.Errorf("invalid path format: missing registry")
}
registry = remainingPath[:secondSlashIndex]
// Extract the artifact and tag from the remaining path
artifactPath := remainingPath[secondSlashIndex+1:]
// Check if the artifactPath contains a ":" for tag and filename
if strings.Contains(artifactPath, ":") {
segments := strings.SplitN(artifactPath, ":", 3)
if len(segments) < 3 {
return "", "", "", "", "", "", fmt.Errorf("invalid artifact format: %s", artifactPath)
}
artifact = segments[0]
tag = segments[1]
fileName = segments[2]
} else {
segments := strings.SplitN(artifactPath, "/", 2)
if len(segments) < 2 {
return "", "", "", "", "", "", fmt.Errorf("invalid artifact format: %s", artifactPath)
}
artifact = segments[0]
tag = segments[1]
fileName = r.FormValue("filename")
if fileName == "" {
return "", "", "", "", "", "", fmt.Errorf("filename not provided in path or form parameter")
}
}
description = r.FormValue("description")
return rootIdentifier, registry, artifact, tag, fileName, description, nil
}
func handleErrors(ctx context.Context, err errcode.Error, w http.ResponseWriter) {
if !commons.IsEmptyError(err) {
w.WriteHeader(err.Code.Descriptor().HTTPStatusCode)
_ = errcode.ServeJSON(w, err)
log.Ctx(ctx).Error().Msgf("Error occurred while performing generic artifact action: %s", err.Message)
}
}
func validatePackageVersionAndFileName(packageName, version, filename string) error {
// Compile the regular expressions
packageNameRe := regexp.MustCompile(packageNameRegex)
versionRe := regexp.MustCompile(versionRegex)
filenameRe := regexp.MustCompile(filenameRegex)
// Validate package name
if !packageNameRe.MatchString(packageName) {
return fmt.Errorf("invalid package name: %s", packageName)
}
// Validate version
if !versionRe.MatchString(version) {
return fmt.Errorf("invalid version: %s", version)
}
// Validate filename
if !filenameRe.MatchString(filename) {
return fmt.Errorf("invalid filename: %s", filename)
}
return nil
}