mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-18 22:57:38 +01:00
Merge pull request #12571 from ywk253100/200723_proxy_cache_secret
Limit the permission of secret used by proxy cache service
This commit is contained in:
commit
518a1721a7
@ -17,8 +17,6 @@ package secret
|
|||||||
const (
|
const (
|
||||||
// JobserviceUser is the name of jobservice user
|
// JobserviceUser is the name of jobservice user
|
||||||
JobserviceUser = "harbor-jobservice"
|
JobserviceUser = "harbor-jobservice"
|
||||||
// ProxyserviceUser is the name of proxyservice user
|
|
||||||
ProxyserviceUser = "harbor-proxyservice"
|
|
||||||
// CoreUser is the name of ui user
|
// CoreUser is the name of ui user
|
||||||
CoreUser = "harbor-core"
|
CoreUser = "harbor-core"
|
||||||
)
|
)
|
||||||
|
96
src/common/security/proxycachesecret/context.go
Normal file
96
src/common/security/proxycachesecret/context.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// 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
|
||||||
|
}
|
108
src/common/security/proxycachesecret/context_test.go
Normal file
108
src/common/security/proxycachesecret/context_test.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// 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 false
|
||||||
}
|
}
|
||||||
return s.store.GetUsername(s.secret) == secret.JobserviceUser ||
|
return s.store.GetUsername(s.secret) == secret.JobserviceUser ||
|
||||||
s.store.GetUsername(s.secret) == secret.CoreUser ||
|
s.store.GetUsername(s.secret) == secret.CoreUser
|
||||||
s.store.GetUsername(s.secret) == secret.ProxyserviceUser
|
|
||||||
}
|
}
|
||||||
|
@ -20,11 +20,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/distribution"
|
"github.com/docker/distribution"
|
||||||
"github.com/docker/distribution/manifest/manifestlist"
|
"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/controller/artifact"
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
"github.com/goharbor/harbor/src/lib"
|
"github.com/goharbor/harbor/src/lib"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
|
"github.com/goharbor/harbor/src/pkg/proxy/secret"
|
||||||
"github.com/goharbor/harbor/src/pkg/registry"
|
"github.com/goharbor/harbor/src/pkg/registry"
|
||||||
"io"
|
"io"
|
||||||
"time"
|
"time"
|
||||||
@ -85,8 +85,7 @@ func (l *localHelper) init() {
|
|||||||
log.Debugf("core url:%s, local core url: %v", config.GetCoreURL(), config.LocalCoreURL())
|
log.Debugf("core url:%s, local core url: %v", config.GetCoreURL(), config.LocalCoreURL())
|
||||||
// the traffic is internal only
|
// the traffic is internal only
|
||||||
registryURL := config.LocalCoreURL()
|
registryURL := config.LocalCoreURL()
|
||||||
authorizer := comHttpAuth.NewSecretAuthorizer(config.ProxyServiceSecret)
|
l.registry = registry.NewClientWithAuthorizer(registryURL, secret.NewAuthorizer(), true)
|
||||||
l.registry = registry.NewClientWithAuthorizer(registryURL, authorizer, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *localHelper) PushBlob(localRepo string, desc distribution.Descriptor, bReader io.ReadCloser) error {
|
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"
|
||||||
"github.com/goharbor/harbor/src/core/promgr/pmsdriver/local"
|
"github.com/goharbor/harbor/src/core/promgr/pmsdriver/local"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -51,8 +49,6 @@ var (
|
|||||||
// defined as a var for testing.
|
// defined as a var for testing.
|
||||||
defaultCACertPath = "/etc/core/ca/ca.crt"
|
defaultCACertPath = "/etc/core/ca/ca.crt"
|
||||||
cfgMgr *comcfg.CfgManager
|
cfgMgr *comcfg.CfgManager
|
||||||
// ProxyServiceSecret is the secret used by proxy service
|
|
||||||
ProxyServiceSecret = utils.GenerateRandomStringWithLen(16)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Init configurations
|
// Init configurations
|
||||||
@ -93,7 +89,6 @@ func initKeyProvider() {
|
|||||||
func initSecretStore() {
|
func initSecretStore() {
|
||||||
m := map[string]string{}
|
m := map[string]string{}
|
||||||
m[JobserviceSecret()] = secret.JobserviceUser
|
m[JobserviceSecret()] = secret.JobserviceUser
|
||||||
m[ProxyServiceSecret] = secret.ProxyserviceUser
|
|
||||||
SecretStore = secret.NewStore(m)
|
SecretStore = secret.NewStore(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/astaxie/beego"
|
"github.com/astaxie/beego"
|
||||||
"github.com/goharbor/harbor/src/pkg/distribution"
|
"github.com/goharbor/harbor/src/pkg/distribution"
|
||||||
"github.com/goharbor/harbor/src/server/middleware"
|
"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/csrf"
|
||||||
"github.com/goharbor/harbor/src/server/middleware/log"
|
"github.com/goharbor/harbor/src/server/middleware/log"
|
||||||
"github.com/goharbor/harbor/src/server/middleware/notification"
|
"github.com/goharbor/harbor/src/server/middleware/notification"
|
||||||
@ -74,6 +75,7 @@ func MiddleWares() []beego.MiddleWare {
|
|||||||
orm.Middleware(),
|
orm.Middleware(),
|
||||||
notification.Middleware(), // notification must ahead of transaction ensure the DB transaction execution complete
|
notification.Middleware(), // notification must ahead of transaction ensure the DB transaction execution complete
|
||||||
transaction.Middleware(dbTxSkippers...),
|
transaction.Middleware(dbTxSkippers...),
|
||||||
|
artifactinfo.Middleware(),
|
||||||
security.Middleware(),
|
security.Middleware(),
|
||||||
readonly.Middleware(readonlySkippers...),
|
readonly.Middleware(readonlySkippers...),
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package middleware
|
package lib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -29,3 +29,33 @@ var (
|
|||||||
// V2CatalogURLRe is the regular expression for mathing the request to v2 handler to list catalog
|
// V2CatalogURLRe is the regular expression for mathing the request to v2 handler to list catalog
|
||||||
V2CatalogURLRe = regexp.MustCompile(`^/v2/_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
|
||||||
|
}
|
68
src/lib/patterns_test.go
Normal file
68
src/lib/patterns_test.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
// 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)
|
||||||
|
}
|
63
src/pkg/proxy/secret/authorizer.go
Normal file
63
src/pkg/proxy/secret/authorizer.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// 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))
|
||||||
|
}
|
50
src/pkg/proxy/secret/authorizer_test.go
Normal file
50
src/pkg/proxy/secret/authorizer_test.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// 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"
|
||||||
"github.com/goharbor/harbor/src/lib/errors"
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
"github.com/goharbor/harbor/src/server/middleware"
|
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -39,10 +38,10 @@ const (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
urlPatterns = map[string]*regexp.Regexp{
|
urlPatterns = map[string]*regexp.Regexp{
|
||||||
"manifest": middleware.V2ManifestURLRe,
|
"manifest": lib.V2ManifestURLRe,
|
||||||
"tag_list": middleware.V2TagListURLRe,
|
"tag_list": lib.V2TagListURLRe,
|
||||||
"blob_upload": middleware.V2BlobUploadURLRe,
|
"blob_upload": lib.V2BlobUploadURLRe,
|
||||||
"blob": middleware.V2BlobURLRe,
|
"blob": lib.V2BlobURLRe,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -56,7 +55,7 @@ func Middleware() func(http.Handler) http.Handler {
|
|||||||
next.ServeHTTP(rw, req)
|
next.ServeHTTP(rw, req)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
repo := m[middleware.RepositorySubexp]
|
repo := m[lib.RepositorySubexp]
|
||||||
pn, err := projectNameFromRepo(repo)
|
pn, err := projectNameFromRepo(repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lib_http.SendError(rw, errors.BadRequestError(err))
|
lib_http.SendError(rw, errors.BadRequestError(err))
|
||||||
@ -66,10 +65,10 @@ func Middleware() func(http.Handler) http.Handler {
|
|||||||
Repository: repo,
|
Repository: repo,
|
||||||
ProjectName: pn,
|
ProjectName: pn,
|
||||||
}
|
}
|
||||||
if d, ok := m[middleware.DigestSubexp]; ok {
|
if d, ok := m[lib.DigestSubexp]; ok {
|
||||||
art.Digest = d
|
art.Digest = d
|
||||||
}
|
}
|
||||||
if ref, ok := m[middleware.ReferenceSubexp]; ok {
|
if ref, ok := m[lib.ReferenceSubexp]; ok {
|
||||||
art.Reference = ref
|
art.Reference = ref
|
||||||
}
|
}
|
||||||
if t, ok := m[tag]; ok {
|
if t, ok := m[tag]; ok {
|
||||||
@ -120,9 +119,9 @@ func parse(url *url.URL) (map[string]string, bool) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if digest.DigestRegexp.MatchString(m[middleware.ReferenceSubexp]) {
|
if digest.DigestRegexp.MatchString(m[lib.ReferenceSubexp]) {
|
||||||
m[middleware.DigestSubexp] = m[middleware.ReferenceSubexp]
|
m[lib.DigestSubexp] = m[lib.ReferenceSubexp]
|
||||||
} else if ref, ok := m[middleware.ReferenceSubexp]; ok {
|
} else if ref, ok := m[lib.ReferenceSubexp]; ok {
|
||||||
m[tag] = ref
|
m[tag] = ref
|
||||||
}
|
}
|
||||||
return m, match
|
return m, match
|
||||||
|
@ -22,7 +22,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/lib"
|
"github.com/goharbor/harbor/src/lib"
|
||||||
"github.com/goharbor/harbor/src/server/middleware"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -45,16 +44,16 @@ func TestParseURL(t *testing.T) {
|
|||||||
{
|
{
|
||||||
input: "/v2/no-project-repo/tags/list",
|
input: "/v2/no-project-repo/tags/list",
|
||||||
expect: map[string]string{
|
expect: map[string]string{
|
||||||
middleware.RepositorySubexp: "no-project-repo",
|
lib.RepositorySubexp: "no-project-repo",
|
||||||
},
|
},
|
||||||
match: true,
|
match: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "/v2/development/golang/manifests/sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
input: "/v2/development/golang/manifests/sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||||
expect: map[string]string{
|
expect: map[string]string{
|
||||||
middleware.RepositorySubexp: "development/golang",
|
lib.RepositorySubexp: "development/golang",
|
||||||
middleware.ReferenceSubexp: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
lib.ReferenceSubexp: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||||
middleware.DigestSubexp: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
lib.DigestSubexp: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||||
},
|
},
|
||||||
match: true,
|
match: true,
|
||||||
},
|
},
|
||||||
@ -67,8 +66,8 @@ func TestParseURL(t *testing.T) {
|
|||||||
{
|
{
|
||||||
input: "/v2/multi/sector/repository/blobs/sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
input: "/v2/multi/sector/repository/blobs/sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||||
expect: map[string]string{
|
expect: map[string]string{
|
||||||
middleware.RepositorySubexp: "multi/sector/repository",
|
lib.RepositorySubexp: "multi/sector/repository",
|
||||||
middleware.DigestSubexp: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
lib.DigestSubexp: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||||
},
|
},
|
||||||
match: true,
|
match: true,
|
||||||
},
|
},
|
||||||
@ -80,23 +79,23 @@ func TestParseURL(t *testing.T) {
|
|||||||
{
|
{
|
||||||
input: "/v2/library/ubuntu/blobs/uploads",
|
input: "/v2/library/ubuntu/blobs/uploads",
|
||||||
expect: map[string]string{
|
expect: map[string]string{
|
||||||
middleware.RepositorySubexp: "library/ubuntu",
|
lib.RepositorySubexp: "library/ubuntu",
|
||||||
},
|
},
|
||||||
match: true,
|
match: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "/v2/library/ubuntu/blobs/uploads/?mount=sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f&from=old/ubuntu",
|
input: "/v2/library/ubuntu/blobs/uploads/?mount=sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f&from=old/ubuntu",
|
||||||
expect: map[string]string{
|
expect: map[string]string{
|
||||||
middleware.RepositorySubexp: "library/ubuntu",
|
lib.RepositorySubexp: "library/ubuntu",
|
||||||
blobMountDigest: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
blobMountDigest: "sha256:08e4a417ff4e3913d8723a05cc34055db01c2fd165b588e049c5bad16ce6094f",
|
||||||
blobMountRepo: "old/ubuntu",
|
blobMountRepo: "old/ubuntu",
|
||||||
},
|
},
|
||||||
match: true,
|
match: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "/v2/library/centos/blobs/uploads/u-12345",
|
input: "/v2/library/centos/blobs/uploads/u-12345",
|
||||||
expect: map[string]string{
|
expect: map[string]string{
|
||||||
middleware.RepositorySubexp: "library/centos",
|
lib.RepositorySubexp: "library/centos",
|
||||||
},
|
},
|
||||||
match: true,
|
match: true,
|
||||||
},
|
},
|
||||||
|
@ -17,8 +17,8 @@ package repoproxy
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/goharbor/harbor/src/common/secret"
|
|
||||||
"github.com/goharbor/harbor/src/common/security"
|
"github.com/goharbor/harbor/src/common/security"
|
||||||
|
"github.com/goharbor/harbor/src/common/security/proxycachesecret"
|
||||||
"github.com/goharbor/harbor/src/lib/errors"
|
"github.com/goharbor/harbor/src/lib/errors"
|
||||||
httpLib "github.com/goharbor/harbor/src/lib/http"
|
httpLib "github.com/goharbor/harbor/src/lib/http"
|
||||||
"github.com/goharbor/harbor/src/replication/model"
|
"github.com/goharbor/harbor/src/replication/model"
|
||||||
@ -158,7 +158,7 @@ func isProxySession(ctx context.Context) bool {
|
|||||||
log.Error("Failed to get security context")
|
log.Error("Failed to get security context")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if sc.IsSolutionUser() && sc.GetUsername() == secret.ProxyserviceUser {
|
if sc.GetUsername() == proxycachesecret.ProxyCacheService {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"github.com/goharbor/harbor/src/common/models"
|
"github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/common/security"
|
"github.com/goharbor/harbor/src/common/security"
|
||||||
|
"github.com/goharbor/harbor/src/common/security/proxycachesecret"
|
||||||
securitySecret "github.com/goharbor/harbor/src/common/security/secret"
|
securitySecret "github.com/goharbor/harbor/src/common/security/secret"
|
||||||
"github.com/goharbor/harbor/src/core/config"
|
"github.com/goharbor/harbor/src/core/config"
|
||||||
"testing"
|
"testing"
|
||||||
@ -59,7 +60,7 @@ func TestIsProxySession(t *testing.T) {
|
|||||||
sc1 := securitySecret.NewSecurityContext("123456789", config.SecretStore)
|
sc1 := securitySecret.NewSecurityContext("123456789", config.SecretStore)
|
||||||
otherCtx := security.NewContext(context.Background(), sc1)
|
otherCtx := security.NewContext(context.Background(), sc1)
|
||||||
|
|
||||||
sc2 := securitySecret.NewSecurityContext(config.ProxyServiceSecret, config.SecretStore)
|
sc2 := proxycachesecret.NewSecurityContext("library/hello-world")
|
||||||
proxyCtx := security.NewContext(context.Background(), sc2)
|
proxyCtx := security.NewContext(context.Background(), sc2)
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
|
42
src/server/middleware/security/proxy_cache_secret.go
Normal file
42
src/server/middleware/security/proxy_cache_secret.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// 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 security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/goharbor/harbor/src/common/security"
|
||||||
|
"github.com/goharbor/harbor/src/common/security/proxycachesecret"
|
||||||
|
"github.com/goharbor/harbor/src/lib"
|
||||||
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
|
ps "github.com/goharbor/harbor/src/pkg/proxy/secret"
|
||||||
|
)
|
||||||
|
|
||||||
|
type proxyCacheSecret struct{}
|
||||||
|
|
||||||
|
func (p *proxyCacheSecret) Generate(req *http.Request) security.Context {
|
||||||
|
log := log.G(req.Context())
|
||||||
|
|
||||||
|
artifact := lib.GetArtifactInfo(req.Context())
|
||||||
|
if artifact == (lib.ArtifactInfo{}) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
secret := ps.GetSecret(req)
|
||||||
|
if !ps.GetManager().Verify(secret, artifact.Repository) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Debugf("a proxy cache secret security context generated for request %s %s", req.Method, req.URL.Path)
|
||||||
|
return proxycachesecret.NewSecurityContext(artifact.Repository)
|
||||||
|
}
|
49
src/server/middleware/security/proxy_cache_secret_test.go
Normal file
49
src/server/middleware/security/proxy_cache_secret_test.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// 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 security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/goharbor/harbor/src/lib"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
ps "github.com/goharbor/harbor/src/pkg/proxy/secret"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProxyCacheSecret(t *testing.T) {
|
||||||
|
psc := &proxyCacheSecret{}
|
||||||
|
|
||||||
|
// request without artifact info in context
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, "http://127.0.0.1/v2/library/hello-world/manifests/latest", nil)
|
||||||
|
sc := psc.Generate(req)
|
||||||
|
assert.Nil(t, sc)
|
||||||
|
|
||||||
|
// request with invalid secret
|
||||||
|
ctx := lib.WithArtifactInfo(req.Context(), lib.ArtifactInfo{
|
||||||
|
Repository: "library/hello-world",
|
||||||
|
})
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Proxy-Cache-Secret %s", "invalid-secret"))
|
||||||
|
sc = psc.Generate(req)
|
||||||
|
assert.Nil(t, sc)
|
||||||
|
|
||||||
|
// pass
|
||||||
|
secret := ps.GetManager().Generate("library/hello-world")
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Proxy-Cache-Secret %s", secret))
|
||||||
|
sc = psc.Generate(req)
|
||||||
|
assert.NotNil(t, sc)
|
||||||
|
}
|
@ -33,6 +33,7 @@ var (
|
|||||||
&robot{},
|
&robot{},
|
||||||
&basicAuth{},
|
&basicAuth{},
|
||||||
&session{},
|
&session{},
|
||||||
|
&proxyCacheSecret{},
|
||||||
&unauthorized{},
|
&unauthorized{},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/goharbor/harbor/src/common/rbac"
|
"github.com/goharbor/harbor/src/common/rbac"
|
||||||
"github.com/goharbor/harbor/src/lib"
|
"github.com/goharbor/harbor/src/lib"
|
||||||
"github.com/goharbor/harbor/src/lib/log"
|
"github.com/goharbor/harbor/src/lib/log"
|
||||||
"github.com/goharbor/harbor/src/server/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type target int
|
type target int
|
||||||
@ -72,7 +71,7 @@ func accessList(req *http.Request) []access {
|
|||||||
})
|
})
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
if len(middleware.V2CatalogURLRe.FindStringSubmatch(req.URL.Path)) == 1 {
|
if len(lib.V2CatalogURLRe.FindStringSubmatch(req.URL.Path)) == 1 {
|
||||||
l = append(l, access{
|
l = append(l, access{
|
||||||
target: catalog,
|
target: catalog,
|
||||||
})
|
})
|
||||||
|
@ -17,7 +17,6 @@ package registry
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/server/middleware/artifactinfo"
|
|
||||||
"github.com/goharbor/harbor/src/server/middleware/blob"
|
"github.com/goharbor/harbor/src/server/middleware/blob"
|
||||||
"github.com/goharbor/harbor/src/server/middleware/contenttrust"
|
"github.com/goharbor/harbor/src/server/middleware/contenttrust"
|
||||||
"github.com/goharbor/harbor/src/server/middleware/immutable"
|
"github.com/goharbor/harbor/src/server/middleware/immutable"
|
||||||
@ -32,7 +31,6 @@ import (
|
|||||||
func RegisterRoutes() {
|
func RegisterRoutes() {
|
||||||
root := router.NewRoute().
|
root := router.NewRoute().
|
||||||
Path("/v2").
|
Path("/v2").
|
||||||
Middleware(artifactinfo.Middleware()).
|
|
||||||
Middleware(v2auth.Middleware())
|
Middleware(v2auth.Middleware())
|
||||||
// catalog
|
// catalog
|
||||||
root.NewRoute().
|
root.NewRoute().
|
||||||
|
Loading…
Reference in New Issue
Block a user