Unverified Commit 518a1721 authored by stonezdj(Daojun Zhang)'s avatar stonezdj(Daojun Zhang) Committed by GitHub
Browse files

Merge pull request #12571 from ywk253100/200723_proxy_cache_secret

Limit the permission of secret used by proxy cache service
parents da662f52 ced7b733
......@@ -17,8 +17,6 @@ package secret
const (
// JobserviceUser is the name of jobservice user
JobserviceUser = "harbor-jobservice"
// ProxyserviceUser is the name of proxyservice user
ProxyserviceUser = "harbor-proxyservice"
// CoreUser is the name of ui user
CoreUser = "harbor-core"
)
......
// 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 proxycachesecret
import (
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/permission/types"
"github.com/goharbor/harbor/src/pkg/project"
)
// const definition
const (
// contains "#" to avoid the conflict with normal user
ProxyCacheService = "harbor#proxy-cache-service"
)
// SecurityContext is the security context for proxy cache secret
type SecurityContext struct {
repository string
mgr project.Manager
}
// NewSecurityContext returns an instance of the proxy cache secret security context
func NewSecurityContext(repository string) *SecurityContext {
return &SecurityContext{
repository: repository,
mgr: project.Mgr,
}
}
// Name returns the name of the security context
func (s *SecurityContext) Name() string {
return "proxy_cache_secret"
}
// IsAuthenticated always returns true
func (s *SecurityContext) IsAuthenticated() bool {
return true
}
// GetUsername returns the name of proxy cache service
func (s *SecurityContext) GetUsername() string {
return ProxyCacheService
}
// IsSysAdmin always returns false
func (s *SecurityContext) IsSysAdmin() bool {
return false
}
// IsSolutionUser always returns false
func (s *SecurityContext) IsSolutionUser() bool {
return false
}
// Can returns true only when requesting pull/push operation against the specific project
func (s *SecurityContext) Can(action types.Action, resource types.Resource) bool {
if !(action == rbac.ActionPull || action == rbac.ActionPush) {
log.Debugf("unauthorized for action %s", action)
return false
}
namespace, ok := rbac.ProjectNamespaceParse(resource)
if !ok {
log.Debugf("got no namespace from the resource %s", resource)
return false
}
project, err := s.mgr.Get(namespace.Identity())
if err != nil {
log.Errorf("failed to get project %v: %v", namespace.Identity(), err)
return false
}
if project == nil {
log.Debugf("project not found %v", namespace.Identity())
return false
}
pro, _ := utils.ParseRepository(s.repository)
if project.Name != pro {
log.Debugf("unauthorized for project %s", project.Name)
return false
}
return true
}
// 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 proxycachesecret
import (
"testing"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/testing/mock"
"github.com/goharbor/harbor/src/testing/pkg/project"
"github.com/stretchr/testify/suite"
)
type proxyCacheSecretTestSuite struct {
suite.Suite
sc *SecurityContext
mgr *project.FakeManager
}
func (p *proxyCacheSecretTestSuite) SetupTest() {
p.mgr = &project.FakeManager{}
p.sc = &SecurityContext{
repository: "library/hello-world",
mgr: p.mgr,
}
}
func (p *proxyCacheSecretTestSuite) TestName() {
p.Equal("proxy_cache_secret", p.sc.Name())
}
func (p *proxyCacheSecretTestSuite) TestIsAuthenticated() {
p.True(p.sc.IsAuthenticated())
}
func (p *proxyCacheSecretTestSuite) TestGetUsername() {
p.Equal(ProxyCacheService, p.sc.GetUsername())
}
func (p *proxyCacheSecretTestSuite) TestIsSysAdmin() {
p.False(p.sc.IsSysAdmin())
}
func (p *proxyCacheSecretTestSuite) TestIsSolutionUser() {
p.False(p.sc.IsSolutionUser())
}
func (p *proxyCacheSecretTestSuite) TestCan() {
// the action isn't pull/push
action := rbac.ActionDelete
resource := rbac.NewProjectNamespace(1).Resource(rbac.ResourceRepository)
p.False(p.sc.Can(action, resource))
// the resource isn't repository
action = rbac.ActionPull
resource = rbac.ResourceConfiguration
p.False(p.sc.Can(action, resource))
// the requested project not found
action = rbac.ActionPull
resource = rbac.NewProjectNamespace(2).Resource(rbac.ResourceRepository)
p.mgr.On("Get", mock.Anything).Return(nil, nil)
p.False(p.sc.Can(action, resource))
p.mgr.AssertExpectations(p.T())
// reset the mock
p.SetupTest()
// pass for action pull
action = rbac.ActionPull
resource = rbac.NewProjectNamespace(1).Resource(rbac.ResourceRepository)
p.mgr.On("Get", mock.Anything).Return(&models.Project{
ProjectID: 1,
Name: "library",
}, nil)
p.True(p.sc.Can(action, resource))
p.mgr.AssertExpectations(p.T())
// reset the mock
p.SetupTest()
// pass for action push
action = rbac.ActionPush
resource = rbac.NewProjectNamespace(1).Resource(rbac.ResourceRepository)
p.mgr.On("Get", mock.Anything).Return(&models.Project{
ProjectID: 1,
Name: "library",
}, nil)
p.True(p.sc.Can(action, resource))
p.mgr.AssertExpectations(p.T())
}
func TestProxyCacheSecretTestSuite(t *testing.T) {
suite.Run(t, &proxyCacheSecretTestSuite{})
}
......@@ -80,6 +80,5 @@ func (s *SecurityContext) Can(action types.Action, resource types.Resource) bool
return false
}
return s.store.GetUsername(s.secret) == secret.JobserviceUser ||
s.store.GetUsername(s.secret) == secret.CoreUser ||
s.store.GetUsername(s.secret) == secret.ProxyserviceUser
s.store.GetUsername(s.secret) == secret.CoreUser
}
......@@ -20,11 +20,11 @@ import (
"fmt"
"github.com/docker/distribution"
"github.com/docker/distribution/manifest/manifestlist"
comHttpAuth "github.com/goharbor/harbor/src/common/http/modifier/auth"
"github.com/goharbor/harbor/src/controller/artifact"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/pkg/proxy/secret"
"github.com/goharbor/harbor/src/pkg/registry"
"io"
"time"
......@@ -85,8 +85,7 @@ func (l *localHelper) init() {
log.Debugf("core url:%s, local core url: %v", config.GetCoreURL(), config.LocalCoreURL())
// the traffic is internal only
registryURL := config.LocalCoreURL()
authorizer := comHttpAuth.NewSecretAuthorizer(config.ProxyServiceSecret)
l.registry = registry.NewClientWithAuthorizer(registryURL, authorizer, true)
l.registry = registry.NewClientWithAuthorizer(registryURL, secret.NewAuthorizer(), true)
}
func (l *localHelper) PushBlob(localRepo string, desc distribution.Descriptor, bReader io.ReadCloser) error {
......
......@@ -30,8 +30,6 @@ import (
"github.com/goharbor/harbor/src/core/promgr"
"github.com/goharbor/harbor/src/core/promgr/pmsdriver/local"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/common/utils"
)
const (
......@@ -51,8 +49,6 @@ var (
// defined as a var for testing.
defaultCACertPath = "/etc/core/ca/ca.crt"
cfgMgr *comcfg.CfgManager
// ProxyServiceSecret is the secret used by proxy service
ProxyServiceSecret = utils.GenerateRandomStringWithLen(16)
)
// Init configurations
......@@ -93,7 +89,6 @@ func initKeyProvider() {
func initSecretStore() {
m := map[string]string{}
m[JobserviceSecret()] = secret.JobserviceUser
m[ProxyServiceSecret] = secret.ProxyserviceUser
SecretStore = secret.NewStore(m)
}
......
......@@ -21,6 +21,7 @@ import (
"github.com/astaxie/beego"
"github.com/goharbor/harbor/src/pkg/distribution"
"github.com/goharbor/harbor/src/server/middleware"
"github.com/goharbor/harbor/src/server/middleware/artifactinfo"
"github.com/goharbor/harbor/src/server/middleware/csrf"
"github.com/goharbor/harbor/src/server/middleware/log"
"github.com/goharbor/harbor/src/server/middleware/notification"
......@@ -74,6 +75,7 @@ func MiddleWares() []beego.MiddleWare {
orm.Middleware(),
notification.Middleware(), // notification must ahead of transaction ensure the DB transaction execution complete
transaction.Middleware(dbTxSkippers...),
artifactinfo.Middleware(),
security.Middleware(),
readonly.Middleware(readonlySkippers...),
}
......
package middleware
package lib
import (
"fmt"
......@@ -29,3 +29,33 @@ var (
// V2CatalogURLRe is the regular expression for mathing the request to v2 handler to list catalog
V2CatalogURLRe = regexp.MustCompile(`^/v2/_catalog$`)
)
// MatchManifestURLPattern checks whether the provided path matches the manifest URL pattern,
// if does, returns the repository and reference as well
func MatchManifestURLPattern(path string) (repository, reference string, match bool) {
strs := V2ManifestURLRe.FindStringSubmatch(path)
if len(strs) < 3 {
return "", "", false
}
return strs[1], strs[2], true
}
// MatchBlobURLPattern checks whether the provided path matches the blob URL pattern,
// if does, returns the repository and reference as well
func MatchBlobURLPattern(path string) (repository, digest string, match bool) {
strs := V2BlobURLRe.FindStringSubmatch(path)
if len(strs) < 3 {
return "", "", false
}
return strs[1], strs[2], true
}
// MatchBlobUploadURLPattern checks whether the provided path matches the blob upload URL pattern,
// if does, returns the repository as well
func MatchBlobUploadURLPattern(path string) (repository string, match bool) {
strs := V2BlobUploadURLRe.FindStringSubmatch(path)
if len(strs) < 2 {
return "", false
}
return strs[1], true
}
// 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 lib
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestMatchManifestURLPattern(t *testing.T) {
_, _, ok := MatchManifestURLPattern("")
assert.False(t, ok)
_, _, ok = MatchManifestURLPattern("/v2/")
assert.False(t, ok)
repository, reference, ok := MatchManifestURLPattern("/v2/library/hello-world/manifests/latest")
assert.True(t, ok)
assert.Equal(t, "library/hello-world", repository)
assert.Equal(t, "latest", reference)
repository, reference, ok = MatchManifestURLPattern("/v2/library/hello-world/manifests/sha256:e5785cb0c62cebbed4965129bae371f0589cadd6d84798fb58c2c5f9e237efd9")
assert.True(t, ok)
assert.Equal(t, "library/hello-world", repository)
assert.Equal(t, "sha256:e5785cb0c62cebbed4965129bae371f0589cadd6d84798fb58c2c5f9e237efd9", reference)
}
func TestMatchBlobURLPattern(t *testing.T) {
_, _, ok := MatchBlobURLPattern("")
assert.False(t, ok)
_, _, ok = MatchBlobURLPattern("/v2/")
assert.False(t, ok)
repository, digest, ok := MatchBlobURLPattern("/v2/library/hello-world/blobs/sha256:e5785cb0c62cebbed4965129bae371f0589cadd6d84798fb58c2c5f9e237efd9")
assert.True(t, ok)
assert.Equal(t, "library/hello-world", repository)
assert.Equal(t, "sha256:e5785cb0c62cebbed4965129bae371f0589cadd6d84798fb58c2c5f9e237efd9", digest)
}
func TestMatchBlobUploadURLPattern(t *testing.T) {
_, ok := MatchBlobUploadURLPattern("")
assert.False(t, ok)
_, ok = MatchBlobUploadURLPattern("/v2/")
assert.False(t, ok)
repository, ok := MatchBlobUploadURLPattern("/v2/library/hello-world/blobs/uploads/")
assert.True(t, ok)
assert.Equal(t, "library/hello-world", repository)
repository, ok = MatchBlobUploadURLPattern("/v2/library/hello-world/blobs/uploads/uuid")
assert.True(t, ok)
assert.Equal(t, "library/hello-world", repository)
}
// 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 secret
import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/goharbor/harbor/src/lib"
)
const (
secretPrefix = "Proxy-Cache-Secret"
)
// NewAuthorizer returns an instance of the authorizer
func NewAuthorizer() lib.Authorizer {
return &authorizer{}
}
type authorizer struct{}
func (s *authorizer) Modify(req *http.Request) error {
if req == nil {
return errors.New("the request is null")
}
repository, _, ok := lib.MatchManifestURLPattern(req.URL.Path)
if !ok {
repository, _, ok = lib.MatchBlobURLPattern(req.URL.Path)
if !ok {
repository, ok = lib.MatchBlobUploadURLPattern(req.URL.Path)
if !ok {
return nil
}
}
}
secret := GetManager().Generate(repository)
req.Header.Set("Authorization", fmt.Sprintf("%s %s", secretPrefix, secret))
return nil
}
// GetSecret gets the secret from the request authorization header
func GetSecret(req *http.Request) string {
auth := req.Header.Get("Authorization")
if !strings.HasPrefix(auth, secretPrefix) {
return ""
}
return strings.TrimSpace(strings.TrimPrefix(auth, secretPrefix))
}
// 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 secret
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/http"
"testing"
)
func TestAuthorizer(t *testing.T) {
authorizer := &authorizer{}
// not manifest/blob requests
req, _ := http.NewRequest(http.MethodGet, "http://127.0.0.1/v2/_catalog", nil)
err := authorizer.Modify(req)
require.Nil(t, err)
assert.Empty(t, GetSecret(req))
// pass, manifest URL
req, _ = http.NewRequest(http.MethodGet, "http://127.0.0.1/v2/library/hello-world/manifests/latest", nil)
err = authorizer.Modify(req)
require.Nil(t, err)
assert.NotEmpty(t, GetSecret(req))
// pass, blob URL
req, _ = http.NewRequest(http.MethodGet, "http://127.0.0.1/v2/library/hello-world/blobs/sha256:e5785cb0c62cebbed4965129bae371f0589cadd6d84798fb58c2c5f9e237efd9", nil)
err = authorizer.Modify(req)
require.Nil(t, err)
assert.NotEmpty(t, GetSecret(req))
// pass, blob upload URL
req, _ = http.NewRequest(http.MethodGet, "http://127.0.0.1/v2/library/hello-world/blobs/uploads/uuid", nil)
err = authorizer.Modify(req)
require.Nil(t, err)
assert.NotEmpty(t, GetSecret(req))
}
......@@ -25,7 +25,6 @@ import (
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/lib/errors"
"github.com/goharbor/harbor/src/lib/log"
"github.com/goharbor/harbor/src/server/middleware"
"github.com/opencontainers/go-digest"
)
......@@ -39,10 +38,10 @@ const (
var (
urlPatterns = map[string]*regexp.Regexp{
"manifest": middleware.V2ManifestURLRe,
"tag_list": middleware.V2TagListURLRe,
"blob_upload": middleware.V2BlobUploadURLRe,
"blob": middleware.V2BlobURLRe,
"manifest": lib.V2ManifestURLRe,
"tag_list": lib.V2TagListURLRe,
"blob_upload": lib.V2BlobUploadURLRe,
"blob": lib.V2BlobURLRe,
}
)
......@@ -56,7 +55,7 @@ func Middleware() func(http.Handler) http.Handler {
next.ServeHTTP(rw, req)
return
}
repo := m[middleware.RepositorySubexp]
repo := m[lib.RepositorySubexp]
pn, err := projectNameFromRepo(repo)
if err != nil {
lib_http.SendError(rw, errors.BadRequestError(err))
......@@ -66,10 +65,10 @@ func Middleware() func(http.Handler) http.Handler {
Repository: repo,
ProjectName: pn,
}
if d, ok := m[middleware.DigestSubexp]; ok {
if d, ok := m[lib.DigestSubexp]; ok {
art.Digest = d
}
if ref, ok := m[middleware.ReferenceSubexp]; ok {
if ref, ok := m[lib.ReferenceSubexp]; ok {
art.Reference = ref
}
if t, ok := m[tag]; ok {
......@@ -120,9 +119,9 @@ func parse(url *url.URL) (map[string]string, bool) {
break
}
}
if digest.DigestRegexp.MatchString(m[middleware.ReferenceSubexp]) {
m[middleware.DigestSubexp] = m[middleware.ReferenceSubexp]
} else if ref, ok := m[middleware.ReferenceSubexp]; ok {
if digest.DigestRegexp.MatchString(m[lib.ReferenceSubexp]) {
m[lib.DigestSubexp] = m[lib.ReferenceSubexp]
} else if ref, ok := m[lib.ReferenceSubexp]; ok {
m[tag] = ref
}
return m, match
......
......@@ -22,7 +22,6 @@ import (
"testing"
"github.com/goharbor/harbor/src/lib"
"github.com/goharbor/harbor/src/server/middleware"
"github.com/stretchr/testify/assert"
)
......@@ -45,16 +44,16 @@ func TestParseURL(t *testing.T) {
{
input: "/v2/no-project-repo/tags/list",
expect: map[string]string{
middleware.RepositorySubexp: "no-project-repo",
lib.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",
lib.RepositorySubexp: "development/golang",
lib.ReferenceSubexp: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
lib.DigestSubexp: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
},
match: true,
},
......@@ -67,8 +66,8 @@ func TestParseURL(t *testing.T) {
{
input: "/v2/multi/s