250 lines
8.0 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 oci
import (
"context"
"net/http"
"net/url"
"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/openapi/contracts/artifact"
"github.com/harness/gitness/registry/app/dist_temp/dcontext"
"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/docker"
v2 "github.com/distribution/distribution/v3/registry/api/v2"
"github.com/opencontainers/go-digest"
"github.com/rs/zerolog/log"
)
func NewHandler(
controller *docker.Controller, spaceStore corestore.SpaceStore, 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 *docker.Controller
SpaceStore corestore.SpaceStore
TokenStore corestore.TokenStore
UserCtrl *usercontroller.Controller
Authenticator authn.Authenticator
URLProvider urlprovider.Provider
Authorizer authz.Authorizer
}
type routeType string
const (
Manifests routeType = "manifests" // /v2/:registry/:image/manifests/:reference.
Blobs routeType = "blobs" // /v2/:registry/:image/blobs/:digest.
BlobsUploadsSession routeType = "blob-uploads-session" // /v2/:registry/:image/blobs/uploads/:session_id.
Tags routeType = "tags" // /v2/:registry/:image/tags/list.
Referrers routeType = "referrers" // /v2/:registry/:image/referrers/:digest.
Invalid routeType = "invalid" // Invalid route.
MinSizeOfURLSegments = 5
APIPartManifest = "manifests"
APIPartBlobs = "blobs"
APIPartUpload = "uploads"
APIPartTag = "tags"
APIPartReferrer = "referrers"
// Add other route types here.
)
func getRouteType(url string) routeType {
url = strings.Trim(url, "/")
segments := strings.Split(url, "/")
if len(segments) < MinSizeOfURLSegments {
return Invalid
}
typ := segments[len(segments)-2]
switch typ {
case APIPartManifest:
return Manifests
case APIPartBlobs:
if segments[len(segments)-1] == APIPartUpload {
return BlobsUploadsSession
}
return Blobs
case APIPartUpload:
return BlobsUploadsSession
case APIPartTag:
return Tags
case APIPartReferrer:
return Referrers
}
return Invalid
}
func GetQueryParamMap(queryParams url.Values) map[string]string {
queryMap := make(map[string]string)
for key, values := range queryParams {
if len(values) > 0 {
queryMap[key] = values[0]
}
}
return queryMap
}
// ExtractPathVars extracts registry, image, reference, digest and tag from the path
// Path format: /v2/:rootSpace/:registry/:image/manifests/:reference (for ex:
// /v2/myRootSpace/reg1/alpine/blobs/sha256:a258b2a6b59a7aa244d8ceab095c7f8df726f27075a69fca7ad8490f3f63148a).
func ExtractPathVars(path string, paramMap map[string]string) (rootIdentifier, registry, image, ref, dgst, tag string) {
path = strings.Trim(path, "/")
segments := strings.Split(path, "/")
rootIdentifier = segments[1]
registry = segments[2]
image = strings.Join(segments[3:len(segments)-2], "/")
typ := getRouteType(path)
switch typ {
case Manifests:
ref = segments[len(segments)-1]
_, err := digest.Parse(ref)
if err != nil {
tag = ref
} else {
dgst = ref
}
case Blobs:
dgst = segments[len(segments)-1]
case BlobsUploadsSession:
if segments[len(segments)-1] != APIPartUpload && segments[len(segments)-2] == APIPartUpload {
image = strings.Join(segments[3:len(segments)-3], "/")
ref = segments[len(segments)-1]
}
if _, ok := paramMap["digest"]; ok {
dgst = paramMap["digest"]
}
case Tags:
// do nothing.
case Referrers:
dgst = segments[len(segments)-1]
case Invalid:
log.Warn().Msgf("Invalid route: %s", path)
default:
log.Warn().Msgf("Unknown route type: %s", typ)
}
log.Debug().Msgf(
"For path: %s, rootIdentifier: %s, registry: %s, image: %s, ref: %s, dgst: %s, tag: %s",
path, rootIdentifier, registry, image, ref, dgst, tag,
)
return rootIdentifier, registry, image, ref, dgst, tag
}
func handleErrors(ctx context.Context, errors errcode.Errors, w http.ResponseWriter) {
if !commons.IsEmpty(errors) {
_ = errcode.ServeJSON(w, errors)
docker.LogError(errors)
log.Ctx(ctx).Error().Errs("OCI errors", errors).Msgf("Error occurred")
} else if status, ok := ctx.Value("http.response.status").(int); ok && status >= 200 && status <= 399 {
dcontext.GetResponseLogger(ctx, log.Info()).Msg("response completed")
}
}
func (h *Handler) getRegistryInfo(r *http.Request, remoteSupport bool) (pkg.RegistryInfo, error) {
ctx := r.Context()
queryParams := r.URL.Query()
path := r.URL.Path
paramMap := GetQueryParamMap(queryParams)
rootIdentifier, registryIdentifier, image, ref, dgst, tag := ExtractPathVars(path, paramMap)
if err := metadata.ValidateIdentifier(rootIdentifier); err != nil {
return pkg.RegistryInfo{}, err
}
rootSpace, err := h.SpaceStore.FindByRef(ctx, rootIdentifier)
if err != nil {
log.Ctx(ctx).Error().Msgf("Root space not found: %s", rootIdentifier)
return pkg.RegistryInfo{}, errcode.ErrCodeRootNotFound
}
registry, err := h.Controller.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.RegistryInfo{}, errcode.ErrCodeRegNotFound
}
_, 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.RegistryInfo{}, errcode.ErrCodeParentNotFound
}
info := &pkg.RegistryInfo{
ArtifactInfo: &pkg.ArtifactInfo{
BaseInfo: &pkg.BaseInfo{
RootIdentifier: rootIdentifier,
RootParentID: rootSpace.ID,
ParentID: registry.ParentID,
},
RegIdentifier: registryIdentifier,
Image: image,
},
Reference: ref,
Digest: dgst,
Tag: tag,
URLBuilder: v2.NewURLBuilderFromRequest(r, false),
Path: r.URL.Path,
}
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.RegistryInfo{}, errcode.ErrCodeParentNotFound
}
if commons.IsEmpty(registryIdentifier) {
log.Ctx(ctx).Warn().Msgf("registry not found in context")
return pkg.RegistryInfo{}, errcode.ErrCodeRegNotFound
}
if !commons.IsEmpty(info.Image) && !commons.IsEmpty(info.Tag) {
flag, err2 := MatchArtifactFilter(registry.AllowedPattern, registry.BlockedPattern, info.Image+":"+info.Tag)
if !flag || err2 != nil {
return pkg.RegistryInfo{}, errcode.ErrCodeDenied
}
}
if registry.Type == artifact.RegistryTypeUPSTREAM && !remoteSupport {
log.Ctx(ctx).Warn().Msgf("Remote registryIdentifier %s not supported", registryIdentifier)
return pkg.RegistryInfo{}, errcode.ErrCodeDenied
}
return *info, nil
}