mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-22 18:25:56 +01:00
add read only mode to stop docker push (#4433)
This commit is contained in:
parent
0b5e0aa041
commit
cbcca015b0
@ -3356,6 +3356,10 @@ definitions:
|
||||
description: >-
|
||||
This attribute restricts what users have the permission to create
|
||||
project. It can be "everyone" or "adminonly".
|
||||
read_only:
|
||||
type: boolean
|
||||
description: >-
|
||||
'docker push' is prohibited by Harbor if you set it to true.
|
||||
self_registration:
|
||||
type: boolean
|
||||
description: >-
|
||||
|
@ -56,3 +56,4 @@ UAA_VERIFY_CERT=$uaa_verify_cert
|
||||
UI_URL=http://ui:8080
|
||||
JOBSERVICE_URL=http://jobservice:8080
|
||||
REGISTRY_STORAGE_PROVIDER_NAME=$storage_provider_name
|
||||
READ_ONLY=false
|
||||
|
@ -48,6 +48,7 @@ var (
|
||||
common.EmailInsecure: true,
|
||||
common.LDAPVerifyCert: true,
|
||||
common.UAAVerifyCert: true,
|
||||
common.ReadOnly: true,
|
||||
}
|
||||
mapKeys = map[string]bool{
|
||||
common.ScanAllPolicy: true,
|
||||
|
@ -152,6 +152,10 @@ var (
|
||||
common.UIURL: "UI_URL",
|
||||
common.JobServiceURL: "JOBSERVICE_URL",
|
||||
common.RegistryStorageProviderName: "REGISTRY_STORAGE_PROVIDER_NAME",
|
||||
common.ReadOnly: &parser{
|
||||
env: "READ_ONLY",
|
||||
parse: parseStringToBool,
|
||||
},
|
||||
}
|
||||
|
||||
// configurations need read from environment variables
|
||||
|
@ -99,4 +99,5 @@ const (
|
||||
RegistryStorageProviderName = "registry_storage_provider_name"
|
||||
UserMember = "u"
|
||||
GroupMember = "g"
|
||||
ReadOnly = "read_only"
|
||||
)
|
||||
|
@ -75,6 +75,7 @@ var adminServerDefaultConfig = map[string]interface{}{
|
||||
common.UAAVerifyCert: false,
|
||||
common.UIURL: "http://myui:8888/",
|
||||
common.JobServiceURL: "http://myjob:8888/",
|
||||
common.ReadOnly: false,
|
||||
}
|
||||
|
||||
// NewAdminserver returns a mock admin server
|
||||
|
@ -60,6 +60,7 @@ var (
|
||||
common.UAAClientSecret,
|
||||
common.UAAEndpoint,
|
||||
common.UAAVerifyCert,
|
||||
common.ReadOnly,
|
||||
}
|
||||
|
||||
stringKeys = []string{
|
||||
@ -97,6 +98,7 @@ var (
|
||||
common.SelfRegistration,
|
||||
common.LDAPVerifyCert,
|
||||
common.UAAVerifyCert,
|
||||
common.ReadOnly,
|
||||
}
|
||||
|
||||
passwordKeys = []string{
|
||||
|
@ -495,3 +495,13 @@ func UAASettings() (*models.UAASettings, error) {
|
||||
}
|
||||
return us, nil
|
||||
}
|
||||
|
||||
// ReadOnly returns a bool to indicates if Harbor is in read only mode.
|
||||
func ReadOnly() bool {
|
||||
cfg, err := mg.Get()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get configuration, will return false as read only, error: %v", err)
|
||||
return false
|
||||
}
|
||||
return cfg[common.ReadOnly].(bool)
|
||||
}
|
||||
|
@ -146,6 +146,9 @@ func TestConfig(t *testing.T) {
|
||||
if !WithAdmiral() {
|
||||
t.Errorf("WithAdmiral should be true")
|
||||
}
|
||||
if ReadOnly() {
|
||||
t.Errorf("ReadOnly should be false")
|
||||
}
|
||||
if AdmiralEndpoint() != "http://www.vmware.com" {
|
||||
t.Errorf("Unexpected admiral endpoint: %s", AdmiralEndpoint())
|
||||
}
|
||||
|
76
src/ui/filter/readonly.go
Normal file
76
src/ui/filter/readonly.go
Normal file
@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// 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 filter
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/astaxie/beego/context"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
)
|
||||
|
||||
const (
|
||||
repoURL = `^/api/repositories/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)(?:[a-z0-9]+(?:[._-][a-z0-9]+)*)$`
|
||||
tagURL = `^/api/repositories/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)tags/([\w][\w.-]{0,127})$`
|
||||
labelURL = `^/api/repositories/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)tags/([\w][\w.-]{0,127})/labels/[0-9]+$`
|
||||
)
|
||||
|
||||
//ReadonlyFilter filters the delete repo/tag request and returns 503.
|
||||
func ReadonlyFilter(ctx *context.Context) {
|
||||
filter(ctx.Request, ctx.ResponseWriter)
|
||||
}
|
||||
|
||||
func filter(req *http.Request, resp http.ResponseWriter) {
|
||||
if !config.ReadOnly() {
|
||||
return
|
||||
}
|
||||
if req.Method != http.MethodDelete {
|
||||
return
|
||||
}
|
||||
if matchRepoTagDelete(req) {
|
||||
resp.WriteHeader(http.StatusServiceUnavailable)
|
||||
}
|
||||
}
|
||||
|
||||
// Only block repository and tag deletion
|
||||
func matchRepoTagDelete(req *http.Request) bool {
|
||||
if inWhiteList(req) {
|
||||
return false
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(tagURL)
|
||||
s := re.FindStringSubmatch(req.URL.Path)
|
||||
if len(s) == 3 {
|
||||
return true
|
||||
}
|
||||
|
||||
re = regexp.MustCompile(repoURL)
|
||||
s = re.FindStringSubmatch(req.URL.Path)
|
||||
if len(s) == 2 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func inWhiteList(req *http.Request) bool {
|
||||
re := regexp.MustCompile(labelURL)
|
||||
s := re.FindStringSubmatch(req.URL.Path)
|
||||
if len(s) == 3 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
83
src/ui/filter/readonly_test.go
Normal file
83
src/ui/filter/readonly_test.go
Normal file
@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// 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 filter
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/vmware/harbor/src/common"
|
||||
utilstest "github.com/vmware/harbor/src/common/utils/test"
|
||||
"github.com/vmware/harbor/src/ui/config"
|
||||
)
|
||||
|
||||
func TestReadonlyFilter(t *testing.T) {
|
||||
|
||||
var defaultConfig = map[string]interface{}{
|
||||
common.ExtEndpoint: "host01.com",
|
||||
common.AUTHMode: "db_auth",
|
||||
common.CfgExpiration: 5,
|
||||
common.TokenExpiration: 30,
|
||||
common.DatabaseType: "mysql",
|
||||
common.MySQLHost: "127.0.0.1",
|
||||
common.MySQLPort: 3306,
|
||||
common.MySQLUsername: "root",
|
||||
common.MySQLPassword: "root123",
|
||||
common.MySQLDatabase: "registry",
|
||||
common.SQLiteFile: "/tmp/registry.db",
|
||||
common.ReadOnly: true,
|
||||
}
|
||||
adminServer, err := utilstest.NewAdminserver(defaultConfig)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer adminServer.Close()
|
||||
if err := os.Setenv("ADMINSERVER_URL", adminServer.URL); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := config.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert := assert.New(t)
|
||||
req1, _ := http.NewRequest("DELETE", "http://127.0.0.1:5000/api/repositories/library/ubuntu", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
filter(req1, rec)
|
||||
assert.Equal(http.StatusServiceUnavailable, rec.Code)
|
||||
|
||||
req2, _ := http.NewRequest("DELETE", "http://127.0.0.1:5000/api/repositories/library/hello-world", nil)
|
||||
rec = httptest.NewRecorder()
|
||||
filter(req2, rec)
|
||||
assert.Equal(http.StatusServiceUnavailable, rec.Code)
|
||||
|
||||
req3, _ := http.NewRequest("DELETE", "http://127.0.0.1:5000/api/repositories/library/hello-world/tags/14.04", nil)
|
||||
rec = httptest.NewRecorder()
|
||||
filter(req3, rec)
|
||||
assert.Equal(http.StatusServiceUnavailable, rec.Code)
|
||||
|
||||
req4, _ := http.NewRequest("DELETE", "http://127.0.0.1:5000/api/repositories/library/hello-world/tags/latest", nil)
|
||||
rec = httptest.NewRecorder()
|
||||
filter(req4, rec)
|
||||
assert.Equal(http.StatusServiceUnavailable, rec.Code)
|
||||
|
||||
req5, _ := http.NewRequest("DELETE", "http://127.0.0.1:5000/api/repositories/library/vmware/hello-world", nil)
|
||||
rec = httptest.NewRecorder()
|
||||
filter(req5, rec)
|
||||
assert.Equal(http.StatusServiceUnavailable, rec.Code)
|
||||
|
||||
}
|
@ -143,6 +143,7 @@ func main() {
|
||||
|
||||
filter.Init()
|
||||
beego.InsertFilter("/*", beego.BeforeRouter, filter.SecurityFilter)
|
||||
beego.InsertFilter("/*", beego.BeforeRouter, filter.ReadonlyFilter)
|
||||
beego.InsertFilter("/api/*", beego.BeforeRouter, filter.MediaTypeFilter("application/json"))
|
||||
|
||||
initRouters()
|
||||
|
@ -154,6 +154,20 @@ func (uh urlHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
uh.next.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
type readonlyHandler struct {
|
||||
next http.Handler
|
||||
}
|
||||
|
||||
func (rh readonlyHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if config.ReadOnly() {
|
||||
if req.Method == http.MethodDelete || req.Method == http.MethodPost || req.Method == http.MethodPatch {
|
||||
http.Error(rw, "Upload/Delete is prohibited in read only mode.", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
}
|
||||
rh.next.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
type listReposHandler struct {
|
||||
next http.Handler
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ func Init(urls ...string) error {
|
||||
return err
|
||||
}
|
||||
Proxy = httputil.NewSingleHostReverseProxy(targetURL)
|
||||
handlers = handlerChain{head: urlHandler{next: listReposHandler{next: contentTrustHandler{next: vulnerableHandler{next: Proxy}}}}}
|
||||
handlers = handlerChain{head: readonlyHandler{next: urlHandler{next: listReposHandler{next: contentTrustHandler{next: vulnerableHandler{next: Proxy}}}}}}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user