mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-18 22:57:38 +01:00
Merge pull request #10571 from reasonerjt/middlware-registry-basic-auth
Add middlewares for permission checking for v2 API
This commit is contained in:
commit
0b468dffb7
@ -62,13 +62,13 @@ func (cth contentTrustHandler) ServeHTTP(rw http.ResponseWriter, req *http.Reque
|
|||||||
util.CopyResp(rec, rw)
|
util.CopyResp(rec, rw)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validate(req *http.Request) (bool, util.ImageInfo) {
|
func validate(req *http.Request) (bool, util.ArtifactInfo) {
|
||||||
var img util.ImageInfo
|
var img util.ArtifactInfo
|
||||||
imgRaw := req.Context().Value(util.ImageInfoCtxKey)
|
imgRaw := req.Context().Value(util.ArtifactInfoCtxKey)
|
||||||
if imgRaw == nil || !config.WithNotary() {
|
if imgRaw == nil || !config.WithNotary() {
|
||||||
return false, img
|
return false, img
|
||||||
}
|
}
|
||||||
img, _ = req.Context().Value(util.ImageInfoCtxKey).(util.ImageInfo)
|
img, _ = req.Context().Value(util.ArtifactInfoCtxKey).(util.ArtifactInfo)
|
||||||
if img.Digest == "" {
|
if img.Digest == "" {
|
||||||
return false, img
|
return false, img
|
||||||
}
|
}
|
||||||
@ -81,7 +81,7 @@ func validate(req *http.Request) (bool, util.ImageInfo) {
|
|||||||
return true, img
|
return true, img
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchNotaryDigest(img util.ImageInfo) (bool, error) {
|
func matchNotaryDigest(img util.ArtifactInfo) (bool, error) {
|
||||||
if NotaryEndpoint == "" {
|
if NotaryEndpoint == "" {
|
||||||
NotaryEndpoint = config.InternalNotaryEndpoint()
|
NotaryEndpoint = config.InternalNotaryEndpoint()
|
||||||
}
|
}
|
||||||
|
@ -49,8 +49,8 @@ func TestMain(m *testing.M) {
|
|||||||
func TestMatchNotaryDigest(t *testing.T) {
|
func TestMatchNotaryDigest(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
// The data from common/utils/notary/helper_test.go
|
// 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"}
|
img1 := util.ArtifactInfo{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"}
|
img2 := util.ArtifactInfo{Repository: "notary-demo/busybox", Reference: "2.0", ProjectName: "notary-demo", Digest: "sha256:12345678"}
|
||||||
|
|
||||||
res1, err := matchNotaryDigest(img1)
|
res1, err := matchNotaryDigest(img1)
|
||||||
assert.Nil(err, "Unexpected error: %v, image: %#v", err, img1)
|
assert.Nil(err, "Unexpected error: %v, image: %#v", err, img1)
|
||||||
|
@ -27,12 +27,12 @@ func New(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
// ServeHTTP ...
|
// ServeHTTP ...
|
||||||
func (r *regTokenHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
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 {
|
if imgRaw == nil {
|
||||||
r.next.ServeHTTP(rw, req)
|
r.next.ServeHTTP(rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
img, _ := req.Context().Value(util.ImageInfoCtxKey).(util.ImageInfo)
|
img, _ := req.Context().Value(util.ArtifactInfoCtxKey).(util.ArtifactInfo)
|
||||||
if img.Digest == "" {
|
if img.Digest == "" {
|
||||||
r.next.ServeHTTP(rw, req)
|
r.next.ServeHTTP(rw, req)
|
||||||
return
|
return
|
||||||
|
@ -20,10 +20,19 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/utils/log"
|
"github.com/goharbor/harbor/src/common/utils/log"
|
||||||
"github.com/goharbor/harbor/src/core/middlewares/util"
|
"github.com/goharbor/harbor/src/core/middlewares/util"
|
||||||
coreutils "github.com/goharbor/harbor/src/core/utils"
|
coreutils "github.com/goharbor/harbor/src/core/utils"
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"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 {
|
type urlHandler struct {
|
||||||
next http.Handler
|
next http.Handler
|
||||||
}
|
}
|
||||||
@ -37,38 +46,65 @@ func New(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
// ServeHTTP ...
|
// ServeHTTP ...
|
||||||
func (uh urlHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (uh urlHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
log.Debugf("in url handler, path: %s", req.URL.Path)
|
path := req.URL.Path
|
||||||
flag, repository, reference := util.MatchPullManifest(req)
|
log.Debugf("in url handler, path: %s", path)
|
||||||
if flag {
|
m, ok := parse(path)
|
||||||
components := strings.SplitN(repository, "/", 2)
|
if !ok {
|
||||||
if len(components) < 2 {
|
uh.next.ServeHTTP(rw, req)
|
||||||
http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", fmt.Sprintf("Bad repository name: %s", repository)), http.StatusBadRequest)
|
}
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
log.Errorf("Error creating repository Client: %v", err)
|
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)
|
http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", fmt.Sprintf("Failed due to internal Error: %v", err)), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
digest, _, err := client.ManifestExist(reference)
|
digest, _, err := client.ManifestExist(art.Reference)
|
||||||
if err != nil {
|
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)
|
http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", fmt.Sprintf("Failed due to internal Error: %v", err)), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
img := util.ImageInfo{
|
art.Digest = digest
|
||||||
Repository: repository,
|
log.Debugf("artifact info of the request: %#v", art)
|
||||||
Reference: reference,
|
ctx := context.WithValue(req.Context(), util.ArtifactInfoCtxKey, art)
|
||||||
ProjectName: components[0],
|
|
||||||
Digest: digest,
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("image info of the request: %#v", img)
|
|
||||||
ctx := context.WithValue(req.Context(), util.ImageInfoCtxKey, img)
|
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
}
|
}
|
||||||
uh.next.ServeHTTP(rw, req)
|
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"
|
||||||
"github.com/docker/distribution/manifest/schema1"
|
"github.com/docker/distribution/manifest/schema1"
|
||||||
"github.com/docker/distribution/manifest/schema2"
|
"github.com/docker/distribution/manifest/schema2"
|
||||||
|
"github.com/docker/distribution/reference"
|
||||||
|
|
||||||
"github.com/garyburd/redigo/redis"
|
"github.com/garyburd/redigo/redis"
|
||||||
"github.com/goharbor/harbor/src/common/dao"
|
"github.com/goharbor/harbor/src/common/dao"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
@ -49,8 +51,15 @@ import (
|
|||||||
type contextKey string
|
type contextKey string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ImageInfoCtxKey the context key for image information
|
// RepositorySubexp is the name for sub regex that maps to repository name in the url
|
||||||
ImageInfoCtxKey = contextKey("ImageInfo")
|
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 the context key for robot account to bypass the pull policy check.
|
||||||
ScannerPullCtxKey = contextKey("ScannerPullCheck")
|
ScannerPullCtxKey = contextKey("ScannerPullCheck")
|
||||||
// TokenUsername ...
|
// TokenUsername ...
|
||||||
@ -73,7 +82,16 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
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 ...
|
// ChartVersionInfo ...
|
||||||
@ -91,8 +109,8 @@ func (info *ChartVersionInfo) MutexKey(suffix ...string) string {
|
|||||||
return strings.Join(append(a, suffix...), ":")
|
return strings.Join(append(a, suffix...), ":")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImageInfo ...
|
// ArtifactInfo ...
|
||||||
type ImageInfo struct {
|
type ArtifactInfo struct {
|
||||||
Repository string
|
Repository string
|
||||||
Reference string
|
Reference string
|
||||||
ProjectName string
|
ProjectName string
|
||||||
@ -281,7 +299,7 @@ func MarshalError(code, msg string) string {
|
|||||||
|
|
||||||
// MatchManifestURL ...
|
// MatchManifestURL ...
|
||||||
func MatchManifestURL(req *http.Request) (bool, string, string) {
|
func MatchManifestURL(req *http.Request) (bool, string, string) {
|
||||||
s := manifestURLRe.FindStringSubmatch(req.URL.Path)
|
s := ManifestURLRe.FindStringSubmatch(req.URL.Path)
|
||||||
if len(s) == 3 {
|
if len(s) == 3 {
|
||||||
s[1] = strings.TrimSuffix(s[1], "/")
|
s[1] = strings.TrimSuffix(s[1], "/")
|
||||||
return true, s[1], s[2]
|
return true, s[1], s[2]
|
||||||
@ -437,8 +455,8 @@ func ChartVersionInfoFromContext(ctx context.Context) (*ChartVersionInfo, bool)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ImageInfoFromContext returns image info from context
|
// ImageInfoFromContext returns image info from context
|
||||||
func ImageInfoFromContext(ctx context.Context) (*ImageInfo, bool) {
|
func ImageInfoFromContext(ctx context.Context) (*ArtifactInfo, bool) {
|
||||||
info, ok := ctx.Value(ImageInfoCtxKey).(*ImageInfo)
|
info, ok := ctx.Value(ArtifactInfoCtxKey).(*ArtifactInfo)
|
||||||
return info, ok
|
return info, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -470,8 +488,8 @@ func NewChartVersionInfoContext(ctx context.Context, info *ChartVersionInfo) con
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewImageInfoContext returns context with image info
|
// NewImageInfoContext returns context with image info
|
||||||
func NewImageInfoContext(ctx context.Context, info *ImageInfo) context.Context {
|
func NewImageInfoContext(ctx context.Context, info *ArtifactInfo) context.Context {
|
||||||
return context.WithValue(ctx, ImageInfoCtxKey, info)
|
return context.WithValue(ctx, ArtifactInfoCtxKey, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManifestInfoContext returns context with manifest 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)
|
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 vs vuln.Severity
|
||||||
var wl models.CVEWhitelist
|
var wl models.CVEWhitelist
|
||||||
var img util.ImageInfo
|
var img util.ArtifactInfo
|
||||||
imgRaw := req.Context().Value(util.ImageInfoCtxKey)
|
imgRaw := req.Context().Value(util.ArtifactInfoCtxKey)
|
||||||
if imgRaw == nil {
|
if imgRaw == nil {
|
||||||
return false, img, vs, wl
|
return false, img, vs, wl
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expected artifact specified?
|
// Expected artifact specified?
|
||||||
img, ok := imgRaw.(util.ImageInfo)
|
img, ok := imgRaw.(util.ArtifactInfo)
|
||||||
if !ok || len(img.Digest) == 0 {
|
if !ok || len(img.Digest) == 0 {
|
||||||
return false, img, vs, wl
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/docker/distribution/reference"
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
|
"regexp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type contextKey string
|
type contextKey string
|
||||||
|
|
||||||
const (
|
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 the context key for manifest info
|
||||||
manifestInfoKey = contextKey("ManifestInfo")
|
manifestInfoKey = contextKey("ManifestInfo")
|
||||||
// ScannerPullCtxKey the context key for robot account to bypass the pull policy check.
|
// ScannerPullCtxKey the context key for robot account to bypass the pull policy check.
|
||||||
ScannerPullCtxKey = contextKey("ScannerPullCheck")
|
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 ...
|
// ManifestInfo ...
|
||||||
type ManifestInfo struct {
|
type ManifestInfo struct {
|
||||||
ProjectID int64
|
ProjectID int64
|
||||||
@ -21,6 +46,23 @@ type ManifestInfo struct {
|
|||||||
Digest string
|
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
|
// NewManifestInfoContext returns context with manifest info
|
||||||
func NewManifestInfoContext(ctx context.Context, info *ManifestInfo) context.Context {
|
func NewManifestInfoContext(ctx context.Context, info *ManifestInfo) context.Context {
|
||||||
return context.WithValue(ctx, manifestInfoKey, info)
|
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
|
// 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) {
|
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)
|
statusCode, payload := serror.APIError(err)
|
||||||
w.WriteHeader(statusCode)
|
w.WriteHeader(statusCode)
|
||||||
w.Write([]byte(payload))
|
w.Write([]byte(payload))
|
||||||
|
@ -73,7 +73,7 @@ class TestProjects(unittest.TestCase):
|
|||||||
#5. Get project quota
|
#5. Get project quota
|
||||||
quota = self.system.get_project_quota("project", TestProjects.project_test_quota_id, **ADMIN_CLIENT)
|
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["count"], 1)
|
||||||
self.assertEqual(quota[0].used["storage"], 2789174)
|
self.assertEqual(quota[0].used["storage"], 2789002)
|
||||||
|
|
||||||
#6. Delete repository(RA) by user(UA);
|
#6. Delete repository(RA) by user(UA);
|
||||||
self.repo.delete_repoitory(TestProjects.repo_name, **ADMIN_CLIENT)
|
self.repo.delete_repoitory(TestProjects.repo_name, **ADMIN_CLIENT)
|
||||||
|
Loading…
Reference in New Issue
Block a user