mirror of
https://github.com/goharbor/harbor
synced 2025-05-14 19:21:09 +00:00
Add middlewares for permission checking for v2 API
When the registry shifts from token auth to basic auth, we'll use the middleware to check permission. This commit add middlewares for populate the artifact info and check permission based on request to /v2/* api via security context Signed-off-by: Daniel Jiang <jiangd@vmware.com>
This commit is contained in:
parent
302b210938
commit
5f8acc3896
@ -62,13 +62,13 @@ func (cth contentTrustHandler) ServeHTTP(rw http.ResponseWriter, req *http.Reque
|
||||
util.CopyResp(rec, rw)
|
||||
}
|
||||
|
||||
func validate(req *http.Request) (bool, util.ImageInfo) {
|
||||
var img util.ImageInfo
|
||||
imgRaw := req.Context().Value(util.ImageInfoCtxKey)
|
||||
func validate(req *http.Request) (bool, util.ArtifactInfo) {
|
||||
var img util.ArtifactInfo
|
||||
imgRaw := req.Context().Value(util.ArtifactInfoCtxKey)
|
||||
if imgRaw == nil || !config.WithNotary() {
|
||||
return false, img
|
||||
}
|
||||
img, _ = req.Context().Value(util.ImageInfoCtxKey).(util.ImageInfo)
|
||||
img, _ = req.Context().Value(util.ArtifactInfoCtxKey).(util.ArtifactInfo)
|
||||
if img.Digest == "" {
|
||||
return false, img
|
||||
}
|
||||
@ -81,7 +81,7 @@ func validate(req *http.Request) (bool, util.ImageInfo) {
|
||||
return true, img
|
||||
}
|
||||
|
||||
func matchNotaryDigest(img util.ImageInfo) (bool, error) {
|
||||
func matchNotaryDigest(img util.ArtifactInfo) (bool, error) {
|
||||
if NotaryEndpoint == "" {
|
||||
NotaryEndpoint = config.InternalNotaryEndpoint()
|
||||
}
|
||||
|
@ -49,8 +49,8 @@ func TestMain(m *testing.M) {
|
||||
func TestMatchNotaryDigest(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
// The data from common/utils/notary/helper_test.go
|
||||
img1 := util.ImageInfo{Repository: "notary-demo/busybox", Reference: "1.0", ProjectName: "notary-demo", Digest: "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7"}
|
||||
img2 := util.ImageInfo{Repository: "notary-demo/busybox", Reference: "2.0", ProjectName: "notary-demo", Digest: "sha256:12345678"}
|
||||
img1 := util.ArtifactInfo{Repository: "notary-demo/busybox", Reference: "1.0", ProjectName: "notary-demo", Digest: "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7"}
|
||||
img2 := util.ArtifactInfo{Repository: "notary-demo/busybox", Reference: "2.0", ProjectName: "notary-demo", Digest: "sha256:12345678"}
|
||||
|
||||
res1, err := matchNotaryDigest(img1)
|
||||
assert.Nil(err, "Unexpected error: %v, image: %#v", err, img1)
|
||||
|
@ -27,12 +27,12 @@ func New(next http.Handler) http.Handler {
|
||||
|
||||
// ServeHTTP ...
|
||||
func (r *regTokenHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
imgRaw := req.Context().Value(util.ImageInfoCtxKey)
|
||||
imgRaw := req.Context().Value(util.ArtifactInfoCtxKey)
|
||||
if imgRaw == nil {
|
||||
r.next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
img, _ := req.Context().Value(util.ImageInfoCtxKey).(util.ImageInfo)
|
||||
img, _ := req.Context().Value(util.ArtifactInfoCtxKey).(util.ArtifactInfo)
|
||||
if img.Digest == "" {
|
||||
r.next.ServeHTTP(rw, req)
|
||||
return
|
||||
|
@ -20,10 +20,19 @@ import (
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||
coreutils "github.com/goharbor/harbor/src/core/utils"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
urlPatterns = []*regexp.Regexp{
|
||||
util.ManifestURLRe, util.TagListURLRe, util.BlobURLRe, util.BlobUploadURLRe,
|
||||
}
|
||||
)
|
||||
|
||||
// urlHandler extracts the artifact info from the url of request to V2 handler and propagates it to context
|
||||
type urlHandler struct {
|
||||
next http.Handler
|
||||
}
|
||||
@ -37,38 +46,65 @@ func New(next http.Handler) http.Handler {
|
||||
|
||||
// ServeHTTP ...
|
||||
func (uh urlHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
log.Debugf("in url handler, path: %s", req.URL.Path)
|
||||
flag, repository, reference := util.MatchPullManifest(req)
|
||||
if flag {
|
||||
components := strings.SplitN(repository, "/", 2)
|
||||
if len(components) < 2 {
|
||||
http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", fmt.Sprintf("Bad repository name: %s", repository)), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
path := req.URL.Path
|
||||
log.Debugf("in url handler, path: %s", path)
|
||||
m, ok := parse(path)
|
||||
if !ok {
|
||||
uh.next.ServeHTTP(rw, req)
|
||||
}
|
||||
repo := m[util.RepositorySubexp]
|
||||
components := strings.SplitN(repo, "/", 2)
|
||||
if len(components) < 2 {
|
||||
http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", fmt.Sprintf("Bad repository name: %s", repo)), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
art := util.ArtifactInfo{
|
||||
Repository: repo,
|
||||
ProjectName: components[0],
|
||||
}
|
||||
if digest, ok := m[util.DigestSubexp]; ok {
|
||||
art.Digest = digest
|
||||
}
|
||||
if ref, ok := m[util.ReferenceSubexp]; ok {
|
||||
art.Reference = ref
|
||||
}
|
||||
|
||||
client, err := coreutils.NewRepositoryClientForUI(util.TokenUsername, repository)
|
||||
if util.ManifestURLRe.MatchString(path) && req.Method == http.MethodGet { // Request for pulling manifest
|
||||
client, err := coreutils.NewRepositoryClientForUI(util.TokenUsername, art.Repository)
|
||||
if err != nil {
|
||||
log.Errorf("Error creating repository Client: %v", err)
|
||||
http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", fmt.Sprintf("Failed due to internal Error: %v", err)), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
digest, _, err := client.ManifestExist(reference)
|
||||
digest, _, err := client.ManifestExist(art.Reference)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get digest for reference: %s, error: %v", reference, err)
|
||||
log.Errorf("Failed to get digest for reference: %s, error: %v", art.Reference, err)
|
||||
http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", fmt.Sprintf("Failed due to internal Error: %v", err)), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
img := util.ImageInfo{
|
||||
Repository: repository,
|
||||
Reference: reference,
|
||||
ProjectName: components[0],
|
||||
Digest: digest,
|
||||
}
|
||||
|
||||
log.Debugf("image info of the request: %#v", img)
|
||||
ctx := context.WithValue(req.Context(), util.ImageInfoCtxKey, img)
|
||||
art.Digest = digest
|
||||
log.Debugf("artifact info of the request: %#v", art)
|
||||
ctx := context.WithValue(req.Context(), util.ArtifactInfoCtxKey, art)
|
||||
req = req.WithContext(ctx)
|
||||
}
|
||||
uh.next.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
func parse(urlPath string) (map[string]string, bool) {
|
||||
m := make(map[string]string)
|
||||
match := false
|
||||
for _, re := range urlPatterns {
|
||||
l := re.FindStringSubmatch(urlPath)
|
||||
if len(l) > 0 {
|
||||
match = true
|
||||
for i := 1; i < len(l); i++ {
|
||||
m[re.SubexpNames()[i]] = l[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
if digest.DigestRegexp.MatchString(m[util.ReferenceSubexp]) {
|
||||
m[util.DigestSubexp] = m[util.ReferenceSubexp]
|
||||
}
|
||||
return m, match
|
||||
}
|
||||
|
101
src/core/middlewares/url/handler_test.go
Normal file
101
src/core/middlewares/url/handler_test.go
Normal file
@ -0,0 +1,101 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// 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 url
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if result := m.Run(); result != 0 {
|
||||
os.Exit(result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseURL(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
expect map[string]string
|
||||
match bool
|
||||
}{
|
||||
{
|
||||
input: "/api/projects",
|
||||
expect: map[string]string{},
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
input: "/v2/_catalog",
|
||||
expect: map[string]string{},
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
input: "/v2/no-project-repo/tags/list",
|
||||
expect: map[string]string{
|
||||
util.RepositorySubexp: "no-project-repo",
|
||||
},
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
input: "/v2/development/golang/manifests/sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||
expect: map[string]string{
|
||||
util.RepositorySubexp: "development/golang",
|
||||
util.ReferenceSubexp: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||
util.DigestSubexp: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||
},
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
input: "/v2/development/golang/manifests/shaxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
expect: map[string]string{},
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
input: "/v2/multi/sector/repository/blobs/sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||
expect: map[string]string{
|
||||
util.RepositorySubexp: "multi/sector/repository",
|
||||
util.DigestSubexp: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||
},
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
input: "/v2/blobs/uploads",
|
||||
expect: map[string]string{},
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
input: "/v2/library/ubuntu/blobs/uploads",
|
||||
expect: map[string]string{
|
||||
util.RepositorySubexp: "library/ubuntu",
|
||||
},
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
input: "/v2/library/centos/blobs/uploads/u-12345",
|
||||
expect: map[string]string{
|
||||
util.RepositorySubexp: "library/centos",
|
||||
},
|
||||
match: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
e, m := parse(c.input)
|
||||
assert.Equal(t, c.match, m)
|
||||
assert.Equal(t, c.expect, e)
|
||||
}
|
||||
}
|
@ -31,6 +31,8 @@ import (
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/docker/distribution/reference"
|
||||
|
||||
"github.com/garyburd/redigo/redis"
|
||||
"github.com/goharbor/harbor/src/common/dao"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
@ -49,8 +51,15 @@ import (
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
// ImageInfoCtxKey the context key for image information
|
||||
ImageInfoCtxKey = contextKey("ImageInfo")
|
||||
// RepositorySubexp is the name for sub regex that maps to repository name in the url
|
||||
RepositorySubexp = "repository"
|
||||
// ReferenceSubexp is the name for sub regex that maps to reference (tag or digest) url
|
||||
ReferenceSubexp = "reference"
|
||||
// DigestSubexp is the name for sub regex that maps to digest in the url
|
||||
DigestSubexp = "digest"
|
||||
|
||||
// ArtifactInfoCtxKey the context key for artifact information
|
||||
ArtifactInfoCtxKey = contextKey("ArtifactInfo")
|
||||
// ScannerPullCtxKey the context key for robot account to bypass the pull policy check.
|
||||
ScannerPullCtxKey = contextKey("ScannerPullCheck")
|
||||
// TokenUsername ...
|
||||
@ -73,7 +82,16 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
manifestURLRe = regexp.MustCompile(`^/v2/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)manifests/([\w][\w.:-]{0,127})`)
|
||||
// ManifestURLRe is the regular expression for matching request v2 handler to view/delete manifest
|
||||
ManifestURLRe = regexp.MustCompile(fmt.Sprintf(`^/v2/(?P<%s>%s)/manifests/(?P<%s>%s|%s)$`, RepositorySubexp, reference.NameRegexp.String(), ReferenceSubexp, reference.TagRegexp.String(), digest.DigestRegexp.String()))
|
||||
// TagListURLRe is the regular expression for matching request to v2 handler to list tags
|
||||
TagListURLRe = regexp.MustCompile(fmt.Sprintf(`^/v2/(?P<%s>%s)/tags/list`, RepositorySubexp, reference.NameRegexp.String()))
|
||||
// BlobURLRe is the regular expression for matching request to v2 handler to retrieve delete a blob
|
||||
BlobURLRe = regexp.MustCompile(fmt.Sprintf(`^/v2/(?P<%s>%s)/blobs/(?P<%s>%s)$`, RepositorySubexp, reference.NameRegexp.String(), DigestSubexp, digest.DigestRegexp.String()))
|
||||
// BlobUploadURLRe is the regular expression for matching the request to v2 handler to upload a blob, the upload uuid currently is not put into a group
|
||||
BlobUploadURLRe = regexp.MustCompile(fmt.Sprintf(`^/v2/(?P<%s>%s)/blobs/uploads[/a-zA-Z0-9\-_\.=]*$`, RepositorySubexp, reference.NameRegexp.String()))
|
||||
// CatalogURLRe is the regular expression for mathing the request to v2 handler to list catalog
|
||||
CatalogURLRe = regexp.MustCompile(`^/v2/_catalog$`)
|
||||
)
|
||||
|
||||
// ChartVersionInfo ...
|
||||
@ -91,8 +109,8 @@ func (info *ChartVersionInfo) MutexKey(suffix ...string) string {
|
||||
return strings.Join(append(a, suffix...), ":")
|
||||
}
|
||||
|
||||
// ImageInfo ...
|
||||
type ImageInfo struct {
|
||||
// ArtifactInfo ...
|
||||
type ArtifactInfo struct {
|
||||
Repository string
|
||||
Reference string
|
||||
ProjectName string
|
||||
@ -281,7 +299,7 @@ func MarshalError(code, msg string) string {
|
||||
|
||||
// MatchManifestURL ...
|
||||
func MatchManifestURL(req *http.Request) (bool, string, string) {
|
||||
s := manifestURLRe.FindStringSubmatch(req.URL.Path)
|
||||
s := ManifestURLRe.FindStringSubmatch(req.URL.Path)
|
||||
if len(s) == 3 {
|
||||
s[1] = strings.TrimSuffix(s[1], "/")
|
||||
return true, s[1], s[2]
|
||||
@ -437,8 +455,8 @@ func ChartVersionInfoFromContext(ctx context.Context) (*ChartVersionInfo, bool)
|
||||
}
|
||||
|
||||
// ImageInfoFromContext returns image info from context
|
||||
func ImageInfoFromContext(ctx context.Context) (*ImageInfo, bool) {
|
||||
info, ok := ctx.Value(ImageInfoCtxKey).(*ImageInfo)
|
||||
func ImageInfoFromContext(ctx context.Context) (*ArtifactInfo, bool) {
|
||||
info, ok := ctx.Value(ArtifactInfoCtxKey).(*ArtifactInfo)
|
||||
return info, ok
|
||||
}
|
||||
|
||||
@ -470,8 +488,8 @@ func NewChartVersionInfoContext(ctx context.Context, info *ChartVersionInfo) con
|
||||
}
|
||||
|
||||
// NewImageInfoContext returns context with image info
|
||||
func NewImageInfoContext(ctx context.Context, info *ImageInfo) context.Context {
|
||||
return context.WithValue(ctx, ImageInfoCtxKey, info)
|
||||
func NewImageInfoContext(ctx context.Context, info *ArtifactInfo) context.Context {
|
||||
return context.WithValue(ctx, ArtifactInfoCtxKey, info)
|
||||
}
|
||||
|
||||
// NewManifestInfoContext returns context with manifest info
|
||||
|
@ -108,17 +108,17 @@ func (vh vulnerableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request)
|
||||
util.CopyResp(rec, rw)
|
||||
}
|
||||
|
||||
func validate(req *http.Request) (bool, util.ImageInfo, vuln.Severity, models.CVEWhitelist) {
|
||||
func validate(req *http.Request) (bool, util.ArtifactInfo, vuln.Severity, models.CVEWhitelist) {
|
||||
var vs vuln.Severity
|
||||
var wl models.CVEWhitelist
|
||||
var img util.ImageInfo
|
||||
imgRaw := req.Context().Value(util.ImageInfoCtxKey)
|
||||
var img util.ArtifactInfo
|
||||
imgRaw := req.Context().Value(util.ArtifactInfoCtxKey)
|
||||
if imgRaw == nil {
|
||||
return false, img, vs, wl
|
||||
}
|
||||
|
||||
// Expected artifact specified?
|
||||
img, ok := imgRaw.(util.ImageInfo)
|
||||
img, ok := imgRaw.(util.ArtifactInfo)
|
||||
if !ok || len(img.Digest) == 0 {
|
||||
return false, img, vs, wl
|
||||
}
|
||||
|
124
src/server/middleware/artifactinfo/artifact_info.go
Normal file
124
src/server/middleware/artifactinfo/artifact_info.go
Normal file
@ -0,0 +1,124 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// 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 artifactinfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||
"github.com/goharbor/harbor/src/server/middleware"
|
||||
reg_err "github.com/goharbor/harbor/src/server/registry/error"
|
||||
"github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
const (
|
||||
blobMountQuery = "mount"
|
||||
blobFromQuery = "from"
|
||||
blobMountDigest = "blob_mount_digest"
|
||||
blobMountRepo = "blob_mount_repo"
|
||||
)
|
||||
|
||||
var (
|
||||
urlPatterns = map[string]*regexp.Regexp{
|
||||
"manifest": middleware.V2ManifestURLRe,
|
||||
"tag_list": middleware.V2TagListURLRe,
|
||||
"blob_upload": middleware.V2BlobUploadURLRe,
|
||||
"blob": middleware.V2BlobURLRe,
|
||||
}
|
||||
)
|
||||
|
||||
// Middleware gets the information of artifact via url of the request and inject it into the context
|
||||
func Middleware() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
log.Debugf("In artifact info middleware, url: %s", req.URL.String())
|
||||
m, ok := parse(req.URL)
|
||||
if !ok {
|
||||
next.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
repo := m[middleware.RepositorySubexp]
|
||||
pn, err := projectNameFromRepo(repo)
|
||||
if err != nil {
|
||||
reg_err.Handle(rw, req, ierror.BadRequestError(err))
|
||||
return
|
||||
}
|
||||
art := &middleware.ArtifactInfo{
|
||||
Repository: repo,
|
||||
ProjectName: pn,
|
||||
}
|
||||
if d, ok := m[middleware.DigestSubexp]; ok {
|
||||
art.Digest = d
|
||||
}
|
||||
if ref, ok := m[middleware.ReferenceSubexp]; ok {
|
||||
art.Reference = ref
|
||||
}
|
||||
|
||||
if bmr, ok := m[blobMountRepo]; ok {
|
||||
// Fail early for now, though in docker registry an invalid may return 202
|
||||
// it's not clear in OCI spec how to handle invalid from parm
|
||||
bmp, err := projectNameFromRepo(bmr)
|
||||
if err != nil {
|
||||
reg_err.Handle(rw, req, ierror.BadRequestError(err))
|
||||
return
|
||||
}
|
||||
art.BlobMountDigest = m[blobMountDigest]
|
||||
art.BlobMountProjectName = bmp
|
||||
art.BlobMountRepository = bmr
|
||||
}
|
||||
ctx := context.WithValue(req.Context(), middleware.ArtifactInfoKey, art)
|
||||
next.ServeHTTP(rw, req.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func projectNameFromRepo(repo string) (string, error) {
|
||||
components := strings.SplitN(repo, "/", 2)
|
||||
if len(components) < 2 {
|
||||
return "", fmt.Errorf("invalid repository name: %s", repo)
|
||||
}
|
||||
return components[0], nil
|
||||
}
|
||||
|
||||
func parse(url *url.URL) (map[string]string, bool) {
|
||||
path := url.Path
|
||||
query := url.Query()
|
||||
m := make(map[string]string)
|
||||
match := false
|
||||
for key, re := range urlPatterns {
|
||||
l := re.FindStringSubmatch(path)
|
||||
if len(l) > 0 {
|
||||
match = true
|
||||
for i := 1; i < len(l); i++ {
|
||||
m[re.SubexpNames()[i]] = l[i]
|
||||
}
|
||||
if key == "blob_upload" && len(query.Get(blobFromQuery)) > 0 {
|
||||
m[blobMountDigest] = query.Get(blobMountQuery)
|
||||
m[blobMountRepo] = query.Get(blobFromQuery)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if digest.DigestRegexp.MatchString(m[middleware.ReferenceSubexp]) {
|
||||
m[middleware.DigestSubexp] = m[middleware.ReferenceSubexp]
|
||||
}
|
||||
return m, match
|
||||
}
|
182
src/server/middleware/artifactinfo/artifact_info_test.go
Normal file
182
src/server/middleware/artifactinfo/artifact_info_test.go
Normal file
@ -0,0 +1,182 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// 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 artifactinfo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/goharbor/harbor/src/server/middleware"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseURL(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
expect map[string]string
|
||||
match bool
|
||||
}{
|
||||
{
|
||||
input: "/api/projects",
|
||||
expect: map[string]string{},
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
input: "/v2/_catalog",
|
||||
expect: map[string]string{},
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
input: "/v2/no-project-repo/tags/list",
|
||||
expect: map[string]string{
|
||||
middleware.RepositorySubexp: "no-project-repo",
|
||||
},
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
input: "/v2/development/golang/manifests/sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||
expect: map[string]string{
|
||||
middleware.RepositorySubexp: "development/golang",
|
||||
middleware.ReferenceSubexp: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||
middleware.DigestSubexp: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||
},
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
input: "/v2/development/golang/manifests/shaxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
expect: map[string]string{},
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
input: "/v2/multi/sector/repository/blobs/sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||
expect: map[string]string{
|
||||
middleware.RepositorySubexp: "multi/sector/repository",
|
||||
middleware.DigestSubexp: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||
},
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
input: "/v2/blobs/uploads",
|
||||
expect: map[string]string{},
|
||||
match: false,
|
||||
},
|
||||
{
|
||||
input: "/v2/library/ubuntu/blobs/uploads",
|
||||
expect: map[string]string{
|
||||
middleware.RepositorySubexp: "library/ubuntu",
|
||||
},
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
input: "/v2/library/ubuntu/blobs/uploads/?mount=sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f&from=old/ubuntu",
|
||||
expect: map[string]string{
|
||||
middleware.RepositorySubexp: "library/ubuntu",
|
||||
blobMountDigest: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||
blobMountRepo: "old/ubuntu",
|
||||
},
|
||||
match: true,
|
||||
},
|
||||
{
|
||||
input: "/v2/library/centos/blobs/uploads/u-12345",
|
||||
expect: map[string]string{
|
||||
middleware.RepositorySubexp: "library/centos",
|
||||
},
|
||||
match: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
url, err := url.Parse(c.input)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
e, m := parse(url)
|
||||
assert.Equal(t, c.match, m)
|
||||
assert.Equal(t, c.expect, e)
|
||||
}
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
h.ctx = req.Context()
|
||||
}
|
||||
func TestPopulateArtifactInfo(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
req *http.Request
|
||||
sc int
|
||||
art *middleware.ArtifactInfo
|
||||
}{
|
||||
{
|
||||
req: httptest.NewRequest(http.MethodDelete, "/v2/hello-world/manifests/latest", nil),
|
||||
sc: http.StatusBadRequest,
|
||||
art: nil,
|
||||
},
|
||||
{
|
||||
req: httptest.NewRequest(http.MethodDelete, "/v2/library/hello-world/manifests/latest", nil),
|
||||
sc: http.StatusOK,
|
||||
art: &middleware.ArtifactInfo{
|
||||
Repository: "library/hello-world",
|
||||
Reference: "latest",
|
||||
ProjectName: "library",
|
||||
},
|
||||
},
|
||||
{
|
||||
req: httptest.NewRequest(http.MethodPost, "/v2/library/ubuntu/blobs/uploads/?mount=sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f&from=no-project", nil),
|
||||
sc: http.StatusBadRequest,
|
||||
art: nil,
|
||||
},
|
||||
{
|
||||
req: httptest.NewRequest(http.MethodPost, "/v2/library/ubuntu/blobs/uploads/?from=old/ubuntu&mount=sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f", nil),
|
||||
sc: http.StatusOK,
|
||||
art: &middleware.ArtifactInfo{
|
||||
Repository: "library/ubuntu",
|
||||
ProjectName: "library",
|
||||
BlobMountRepository: "old/ubuntu",
|
||||
BlobMountDigest: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||
BlobMountProjectName: "old",
|
||||
},
|
||||
},
|
||||
{
|
||||
req: httptest.NewRequest(http.MethodDelete, "/v2/library/hello-world/manifests/sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f", nil),
|
||||
sc: http.StatusOK,
|
||||
art: &middleware.ArtifactInfo{
|
||||
Repository: "library/hello-world",
|
||||
Reference: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||
Digest: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||
ProjectName: "library",
|
||||
},
|
||||
},
|
||||
}
|
||||
next := &handler{}
|
||||
|
||||
for _, tt := range cases {
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
Middleware()(next).ServeHTTP(rec, tt.req)
|
||||
assert.Equal(t, tt.sc, rec.Code)
|
||||
if tt.art != nil {
|
||||
a, ok := middleware.ArtifactInfoFromContext(next.ctx)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, *tt.art, *a)
|
||||
}
|
||||
}
|
||||
}
|
@ -2,17 +2,42 @@ package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
// RepositorySubexp is the name for sub regex that maps to repository name in the url
|
||||
RepositorySubexp = "repository"
|
||||
// ReferenceSubexp is the name for sub regex that maps to reference (tag or digest) url
|
||||
ReferenceSubexp = "reference"
|
||||
// DigestSubexp is the name for sub regex that maps to digest in the url
|
||||
DigestSubexp = "digest"
|
||||
// ArtifactInfoKey the context key for artifact info
|
||||
ArtifactInfoKey = contextKey("artifactInfo")
|
||||
// manifestInfoKey the context key for manifest info
|
||||
manifestInfoKey = contextKey("ManifestInfo")
|
||||
// ScannerPullCtxKey the context key for robot account to bypass the pull policy check.
|
||||
ScannerPullCtxKey = contextKey("ScannerPullCheck")
|
||||
)
|
||||
|
||||
var (
|
||||
// V2ManifestURLRe is the regular expression for matching request v2 handler to view/delete manifest
|
||||
V2ManifestURLRe = regexp.MustCompile(fmt.Sprintf(`^/v2/(?P<%s>%s)/manifests/(?P<%s>%s|%s)$`, RepositorySubexp, reference.NameRegexp.String(), ReferenceSubexp, reference.TagRegexp.String(), digest.DigestRegexp.String()))
|
||||
// V2TagListURLRe is the regular expression for matching request to v2 handler to list tags
|
||||
V2TagListURLRe = regexp.MustCompile(fmt.Sprintf(`^/v2/(?P<%s>%s)/tags/list`, RepositorySubexp, reference.NameRegexp.String()))
|
||||
// V2BlobURLRe is the regular expression for matching request to v2 handler to retrieve delete a blob
|
||||
V2BlobURLRe = regexp.MustCompile(fmt.Sprintf(`^/v2/(?P<%s>%s)/blobs/(?P<%s>%s)$`, RepositorySubexp, reference.NameRegexp.String(), DigestSubexp, digest.DigestRegexp.String()))
|
||||
// V2BlobUploadURLRe is the regular expression for matching the request to v2 handler to upload a blob, the upload uuid currently is not put into a group
|
||||
V2BlobUploadURLRe = regexp.MustCompile(fmt.Sprintf(`^/v2/(?P<%s>%s)/blobs/uploads[/a-zA-Z0-9\-_\.=]*$`, RepositorySubexp, reference.NameRegexp.String()))
|
||||
// V2CatalogURLRe is the regular expression for mathing the request to v2 handler to list catalog
|
||||
V2CatalogURLRe = regexp.MustCompile(`^/v2/_catalog$`)
|
||||
)
|
||||
|
||||
// ManifestInfo ...
|
||||
type ManifestInfo struct {
|
||||
ProjectID int64
|
||||
@ -21,6 +46,23 @@ type ManifestInfo struct {
|
||||
Digest string
|
||||
}
|
||||
|
||||
// ArtifactInfo ...
|
||||
type ArtifactInfo struct {
|
||||
Repository string
|
||||
Reference string
|
||||
ProjectName string
|
||||
Digest string
|
||||
BlobMountRepository string
|
||||
BlobMountProjectName string
|
||||
BlobMountDigest string
|
||||
}
|
||||
|
||||
// ArtifactInfoFromContext returns the artifact info from context
|
||||
func ArtifactInfoFromContext(ctx context.Context) (*ArtifactInfo, bool) {
|
||||
info, ok := ctx.Value(ArtifactInfoKey).(*ArtifactInfo)
|
||||
return info, ok
|
||||
}
|
||||
|
||||
// NewManifestInfoContext returns context with manifest info
|
||||
func NewManifestInfoContext(ctx context.Context, info *ManifestInfo) context.Context {
|
||||
return context.WithValue(ctx, manifestInfoKey, info)
|
||||
|
116
src/server/middleware/v2authz/authz.go
Normal file
116
src/server/middleware/v2authz/authz.go
Normal file
@ -0,0 +1,116 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// 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 authz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/core/filter"
|
||||
"github.com/goharbor/harbor/src/core/promgr"
|
||||
ierror "github.com/goharbor/harbor/src/internal/error"
|
||||
"github.com/goharbor/harbor/src/server/middleware"
|
||||
reg_err "github.com/goharbor/harbor/src/server/registry/error"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type reqChecker struct {
|
||||
pm promgr.ProjectManager
|
||||
}
|
||||
|
||||
func (rc *reqChecker) check(req *http.Request) error {
|
||||
securityCtx, err := filter.GetSecurityContext(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if a, ok := middleware.ArtifactInfoFromContext(req.Context()); ok {
|
||||
action := getAction(req)
|
||||
if action == "" {
|
||||
return nil
|
||||
}
|
||||
log.Debugf("action: %s, repository: %s", action, a.Repository)
|
||||
pid, err := rc.projectID(a.ProjectName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resource := rbac.NewProjectNamespace(pid).Resource(rbac.ResourceRepository)
|
||||
if !securityCtx.Can(action, resource) {
|
||||
return fmt.Errorf("unauthorized to access repository: %s, action: %s", a.Repository, action)
|
||||
}
|
||||
if req.Method == http.MethodPost && a.BlobMountProjectName != "" { // check permission for the source of blob mount
|
||||
p, err := rc.pm.Get(a.BlobMountProjectName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resource := rbac.NewProjectNamespace(p.ProjectID).Resource(rbac.ResourceRepository)
|
||||
if !securityCtx.Can(rbac.ActionPull, resource) {
|
||||
return fmt.Errorf("unauthorized to access repository from which to mount blob: %s, action: %s", a.BlobMountRepository, rbac.ActionPull)
|
||||
}
|
||||
}
|
||||
} else if len(middleware.V2CatalogURLRe.FindStringSubmatch(req.URL.Path)) == 1 && !securityCtx.IsSysAdmin() {
|
||||
return fmt.Errorf("unauthorized to list catalog")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *reqChecker) projectID(name string) (int64, error) {
|
||||
p, err := rc.pm.Get(name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if p == nil {
|
||||
return 0, fmt.Errorf("project not found, name: %s", name)
|
||||
}
|
||||
return p.ProjectID, nil
|
||||
}
|
||||
|
||||
func getAction(req *http.Request) rbac.Action {
|
||||
pushActions := map[string]struct{}{
|
||||
http.MethodPost: {},
|
||||
http.MethodDelete: {},
|
||||
http.MethodPatch: {},
|
||||
http.MethodPut: {},
|
||||
}
|
||||
pullActions := map[string]struct{}{
|
||||
http.MethodGet: {},
|
||||
http.MethodHead: {},
|
||||
}
|
||||
if _, ok := pushActions[req.Method]; ok {
|
||||
return rbac.ActionPush
|
||||
}
|
||||
if _, ok := pullActions[req.Method]; ok {
|
||||
return rbac.ActionPull
|
||||
}
|
||||
return ""
|
||||
|
||||
}
|
||||
|
||||
var checker = reqChecker{
|
||||
pm: config.GlobalProjectMgr,
|
||||
}
|
||||
|
||||
// Middleware checks the permission of the request to access the artifact
|
||||
func Middleware() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if err := checker.check(req); err != nil {
|
||||
reg_err.Handle(rw, req, ierror.UnauthorizedError(err))
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(rw, req)
|
||||
})
|
||||
}
|
||||
}
|
212
src/server/middleware/v2authz/authz_test.go
Normal file
212
src/server/middleware/v2authz/authz_test.go
Normal file
@ -0,0 +1,212 @@
|
||||
// Copyright Project Harbor Authors
|
||||
//
|
||||
// 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 authz
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/rbac"
|
||||
"github.com/goharbor/harbor/src/core/filter"
|
||||
"github.com/goharbor/harbor/src/core/promgr/metamgr"
|
||||
"github.com/goharbor/harbor/src/server/middleware"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type mockPM struct{}
|
||||
|
||||
func (mockPM) Get(projectIDOrName interface{}) (*models.Project, error) {
|
||||
name := projectIDOrName.(string)
|
||||
id, _ := strconv.Atoi(strings.TrimPrefix(name, "project_"))
|
||||
if id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return &models.Project{
|
||||
ProjectID: int64(id),
|
||||
Name: name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (mockPM) Create(*models.Project) (int64, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (mockPM) Delete(projectIDOrName interface{}) error {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (mockPM) Update(projectIDOrName interface{}, project *models.Project) error {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (mockPM) List(query *models.ProjectQueryParam) (*models.ProjectQueryResult, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (mockPM) IsPublic(projectIDOrName interface{}) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (mockPM) Exists(projectIDOrName interface{}) (bool, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (mockPM) GetPublic() ([]*models.Project, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (mockPM) GetMetadataManager() metamgr.ProjectMetadataManager {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
type mockSC struct{}
|
||||
|
||||
func (mockSC) IsAuthenticated() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (mockSC) GetUsername() string {
|
||||
return "mock"
|
||||
}
|
||||
|
||||
func (mockSC) IsSysAdmin() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (mockSC) IsSolutionUser() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (mockSC) GetMyProjects() ([]*models.Project, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (mockSC) GetProjectRoles(projectIDOrName interface{}) []int {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (mockSC) Can(action rbac.Action, resource rbac.Resource) bool {
|
||||
ns, _ := resource.GetNamespace()
|
||||
perms := map[int64]map[rbac.Action]struct{}{
|
||||
1: {
|
||||
rbac.ActionPull: {},
|
||||
rbac.ActionPush: {},
|
||||
},
|
||||
2: {
|
||||
rbac.ActionPull: {},
|
||||
},
|
||||
}
|
||||
pid := ns.Identity().(int64)
|
||||
m, ok := perms[pid]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
_, ok = m[action]
|
||||
return ok
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
checker = reqChecker{
|
||||
pm: mockPM{},
|
||||
}
|
||||
if rc := m.Run(); rc != 0 {
|
||||
os.Exit(rc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiddleware(t *testing.T) {
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
})
|
||||
|
||||
baseCtx := context.WithValue(context.Background(), filter.SecurCtxKey, mockSC{})
|
||||
ar1 := &middleware.ArtifactInfo{
|
||||
Repository: "project_1/hello-world",
|
||||
Reference: "v1",
|
||||
ProjectName: "project_1",
|
||||
}
|
||||
ar2 := &middleware.ArtifactInfo{
|
||||
Repository: "library/ubuntu",
|
||||
Reference: "14.04",
|
||||
ProjectName: "library",
|
||||
}
|
||||
ar3 := &middleware.ArtifactInfo{
|
||||
Repository: "project_1/ubuntu",
|
||||
Reference: "14.04",
|
||||
ProjectName: "project_1",
|
||||
BlobMountRepository: "project_2/ubuntu",
|
||||
BlobMountProjectName: "project_2",
|
||||
BlobMountDigest: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||
}
|
||||
ar4 := &middleware.ArtifactInfo{
|
||||
Repository: "project_1/ubuntu",
|
||||
Reference: "14.04",
|
||||
ProjectName: "project_1",
|
||||
BlobMountRepository: "project_3/ubuntu",
|
||||
BlobMountProjectName: "project_3",
|
||||
BlobMountDigest: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||
}
|
||||
ctx1 := context.WithValue(baseCtx, middleware.ArtifactInfoKey, ar1)
|
||||
ctx2 := context.WithValue(baseCtx, middleware.ArtifactInfoKey, ar2)
|
||||
ctx3 := context.WithValue(baseCtx, middleware.ArtifactInfoKey, ar3)
|
||||
ctx4 := context.WithValue(baseCtx, middleware.ArtifactInfoKey, ar4)
|
||||
req1a, _ := http.NewRequest(http.MethodGet, "/v2/project_1/hello-world/manifest/v1", nil)
|
||||
req1b, _ := http.NewRequest(http.MethodDelete, "/v2/project_1/hello-world/manifest/v1", nil)
|
||||
req2, _ := http.NewRequest(http.MethodGet, "/v2/library/ubuntu/manifest/14.04", nil)
|
||||
req3, _ := http.NewRequest(http.MethodGet, "/v2/_catalog", nil)
|
||||
req4, _ := http.NewRequest(http.MethodPost, "/v2/project_1/ubuntu/blobs/uploads/mount=?mount=sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f&from=project_2/ubuntu", nil)
|
||||
req5, _ := http.NewRequest(http.MethodPost, "/v2/project_1/ubuntu/blobs/uploads/mount=?mount=sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f&from=project_3/ubuntu", nil)
|
||||
|
||||
cases := []struct {
|
||||
input *http.Request
|
||||
status int
|
||||
}{
|
||||
{
|
||||
input: req1a.WithContext(ctx1),
|
||||
status: http.StatusOK,
|
||||
},
|
||||
{
|
||||
input: req1b.WithContext(ctx1),
|
||||
status: http.StatusOK,
|
||||
},
|
||||
{
|
||||
input: req2.WithContext(ctx2),
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
input: req3.WithContext(baseCtx),
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
{
|
||||
input: req4.WithContext(ctx3),
|
||||
status: http.StatusOK,
|
||||
},
|
||||
{
|
||||
input: req5.WithContext(ctx4),
|
||||
status: http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
rec := httptest.NewRecorder()
|
||||
t.Logf("req : %s, %s", c.input.Method, c.input.URL)
|
||||
Middleware()(next).ServeHTTP(rec, c.input)
|
||||
assert.Equal(t, c.status, rec.Result().StatusCode)
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ import (
|
||||
|
||||
// Handle generates the HTTP status code and error payload and writes them to the response
|
||||
func Handle(w http.ResponseWriter, req *http.Request, err error) {
|
||||
log.Errorf("failed to handle the request %s: %v", req.URL.Path, err)
|
||||
log.Errorf("failed to handle the request %s: %v", req.URL, err)
|
||||
statusCode, payload := serror.APIError(err)
|
||||
w.WriteHeader(statusCode)
|
||||
w.Write([]byte(payload))
|
||||
|
@ -73,7 +73,7 @@ class TestProjects(unittest.TestCase):
|
||||
#5. Get project quota
|
||||
quota = self.system.get_project_quota("project", TestProjects.project_test_quota_id, **ADMIN_CLIENT)
|
||||
self.assertEqual(quota[0].used["count"], 1)
|
||||
self.assertEqual(quota[0].used["storage"], 2789174)
|
||||
self.assertEqual(quota[0].used["storage"], 2789002)
|
||||
|
||||
#6. Delete repository(RA) by user(UA);
|
||||
self.repo.delete_repoitory(TestProjects.repo_name, **ADMIN_CLIENT)
|
||||
|
Loading…
x
Reference in New Issue
Block a user