add read only mode to stop docker push (#4433)

This commit is contained in:
Yan 2018-03-23 03:16:08 -07:00 committed by GitHub
parent 0b5e0aa041
commit cbcca015b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 202 additions and 1 deletions

View File

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

View File

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

View File

@ -48,6 +48,7 @@ var (
common.EmailInsecure: true,
common.LDAPVerifyCert: true,
common.UAAVerifyCert: true,
common.ReadOnly: true,
}
mapKeys = map[string]bool{
common.ScanAllPolicy: true,

View File

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

View File

@ -99,4 +99,5 @@ const (
RegistryStorageProviderName = "registry_storage_provider_name"
UserMember = "u"
GroupMember = "g"
ReadOnly = "read_only"
)

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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