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:
Daniel Jiang 2020-01-20 15:02:58 +08:00
parent 302b210938
commit 5f8acc3896
14 changed files with 876 additions and 45 deletions

View File

@ -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()
}

View File

@ -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)

View File

@ -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

View File

@ -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
}

View 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)
}
}

View File

@ -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

View File

@ -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
}

View 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
}

View 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)
}
}
}

View File

@ -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)

View 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)
})
}
}

View 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)
}
}

View File

@ -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))

View File

@ -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)