Merge pull request #10571 from reasonerjt/middlware-registry-basic-auth

Add middlewares for permission checking for v2 API
This commit is contained in:
Daniel Jiang 2020-01-28 01:46:09 +08:00 committed by GitHub
commit 0b468dffb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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) 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()
} }

View File

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

View File

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

View File

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

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

View File

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

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

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

View File

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