Merge pull request #9427 from wy65701436/immutable-middleware

add immutable tag middleware
This commit is contained in:
Wang Yan 2019-10-17 20:28:34 +08:00 committed by GitHub
commit 51d3134e4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 292 additions and 54 deletions

View File

@ -21,6 +21,7 @@ import (
"github.com/goharbor/harbor/src/core/middlewares/chart"
"github.com/goharbor/harbor/src/core/middlewares/contenttrust"
"github.com/goharbor/harbor/src/core/middlewares/countquota"
"github.com/goharbor/harbor/src/core/middlewares/immutable"
"github.com/goharbor/harbor/src/core/middlewares/listrepo"
"github.com/goharbor/harbor/src/core/middlewares/multiplmanifest"
"github.com/goharbor/harbor/src/core/middlewares/readonly"
@ -70,6 +71,7 @@ func (b *DefaultCreator) geMiddleware(mName string) alice.Constructor {
VULNERABLE: func(next http.Handler) http.Handler { return vulnerable.New(next) },
SIZEQUOTA: func(next http.Handler) http.Handler { return sizequota.New(next) },
COUNTQUOTA: func(next http.Handler) http.Handler { return countquota.New(next) },
IMMUTABLE: func(next http.Handler) http.Handler { return immutable.New(next) },
}
return middlewares[mName]
}

View File

@ -25,13 +25,14 @@ const (
VULNERABLE = "vulnerable"
SIZEQUOTA = "sizequota"
COUNTQUOTA = "countquota"
IMMUTABLE = "immutable"
)
// ChartMiddlewares middlewares for chart server
var ChartMiddlewares = []string{CHART}
// Middlewares with sequential organization
var Middlewares = []string{READONLY, URL, MUITIPLEMANIFEST, LISTREPO, CONTENTTRUST, VULNERABLE, SIZEQUOTA, COUNTQUOTA}
var Middlewares = []string{READONLY, URL, MUITIPLEMANIFEST, LISTREPO, CONTENTTRUST, VULNERABLE, SIZEQUOTA, IMMUTABLE, COUNTQUOTA}
// MiddlewaresLocal ...
var MiddlewaresLocal = []string{SIZEQUOTA, COUNTQUOTA}

View File

@ -0,0 +1,93 @@
// 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 immutable
import (
"fmt"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
common_util "github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/middlewares/util"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
"net/http"
)
type immutableHandler struct {
next http.Handler
}
// New ...
func New(next http.Handler) http.Handler {
return &immutableHandler{
next: next,
}
}
// ServeHTTP ...
func (rh immutableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if match, _, _ := util.MatchPushManifest(req); !match {
rh.next.ServeHTTP(rw, req)
return
}
info, ok := util.ManifestInfoFromContext(req.Context())
if !ok {
var err error
info, err = util.ParseManifestInfoFromPath(req)
if err != nil {
log.Error(err)
rh.next.ServeHTTP(rw, req)
return
}
}
_, repoName := common_util.ParseRepository(info.Repository)
matched, err := rule.NewRuleMatcher(info.ProjectID).Match(art.Candidate{
Repository: repoName,
Tag: info.Tag,
NamespaceID: info.ProjectID,
})
if err != nil {
log.Error(err)
rh.next.ServeHTTP(rw, req)
return
}
if !matched {
rh.next.ServeHTTP(rw, req)
return
}
artifactQuery := &models.ArtifactQuery{
PID: info.ProjectID,
Repo: info.Repository,
Tag: info.Tag,
}
afs, err := dao.ListArtifacts(artifactQuery)
if err != nil {
log.Error(err)
rh.next.ServeHTTP(rw, req)
return
}
if len(afs) == 0 {
rh.next.ServeHTTP(rw, req)
return
}
// rule matched and non-existent is a immutable tag
http.Error(rw, util.MarshalError("DENIED",
fmt.Sprintf("The tag:%s:%s is immutable, cannot be overwrite.", info.Repository, info.Tag)), http.StatusPreconditionFailed)
return
}

View File

@ -0,0 +1,151 @@
package immutable
import (
"github.com/docker/distribution"
"github.com/goharbor/harbor/src/core/middlewares/util"
"github.com/opencontainers/go-digest"
"fmt"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/pkg/immutabletag"
immu_model "github.com/goharbor/harbor/src/pkg/immutabletag/model"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"math/rand"
"net/http"
"net/http/httptest"
"os"
"testing"
)
type HandlerSuite struct {
suite.Suite
}
func doPutManifestRequest(projectID int64, projectName, name, tag, dgt string, next ...http.HandlerFunc) int {
repository := fmt.Sprintf("%s/%s", projectName, name)
url := fmt.Sprintf("/v2/%s/manifests/%s", repository, tag)
req, _ := http.NewRequest("PUT", url, nil)
mfInfo := &util.ManifestInfo{
ProjectID: projectID,
Repository: repository,
Tag: tag,
Digest: dgt,
References: []distribution.Descriptor{
{Digest: digest.FromString(randomString(15))},
{Digest: digest.FromString(randomString(15))},
},
}
ctx := util.NewManifestInfoContext(req.Context(), mfInfo)
rr := httptest.NewRecorder()
var n http.HandlerFunc
if len(next) > 0 {
n = next[0]
} else {
n = func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusCreated)
}
}
h := New(http.HandlerFunc(n))
h.ServeHTTP(util.NewCustomResponseWriter(rr), req.WithContext(ctx))
return rr.Code
}
func randomString(n int) string {
const letterBytes = "abcdefghijklmnopqrstuvwxyz"
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}
func (suite *HandlerSuite) addProject(projectName string) int64 {
projectID, err := dao.AddProject(models.Project{
Name: projectName,
OwnerID: 1,
})
suite.Nil(err, fmt.Sprintf("Add project failed for %s", projectName))
return projectID
}
func (suite *HandlerSuite) addArt(pid int64, repo string, tag string) int64 {
afid, err := dao.AddArtifact(&models.Artifact{
PID: pid,
Repo: repo,
Tag: tag,
Digest: digest.FromString(randomString(15)).String(),
Kind: "Docker-Image",
})
suite.Nil(err, fmt.Sprintf("Add artifact failed for %s", repo))
return afid
}
func (suite *HandlerSuite) addImmutableRule(pid int64) int64 {
metadata := &immu_model.Metadata{
ProjectID: pid,
Priority: 1,
Action: "immutable",
Template: "immutable_template",
TagSelectors: []*immu_model.Selector{
{
Kind: "doublestar",
Decoration: "matches",
Pattern: "release-**",
},
},
ScopeSelectors: map[string][]*immu_model.Selector{
"repository": {
{
Kind: "doublestar",
Decoration: "repoMatches",
Pattern: "**",
},
},
},
}
id, err := immutabletag.ImmuCtr.CreateImmutableRule(metadata)
require.NoError(suite.T(), err, "nil error expected but got %s", err)
return id
}
func (suite *HandlerSuite) TestPutManifestCreated() {
projectName := randomString(5)
projectID := suite.addProject(projectName)
immuRuleID := suite.addImmutableRule(projectID)
afID := suite.addArt(projectID, projectName+"/photon", "release-1.10")
defer func() {
dao.DeleteProject(projectID)
dao.DeleteArtifact(afID)
immutabletag.ImmuCtr.DeleteImmutableRule(immuRuleID)
}()
dgt := digest.FromString(randomString(15)).String()
code1 := doPutManifestRequest(projectID, projectName, "photon", "release-1.10", dgt)
suite.Equal(http.StatusPreconditionFailed, code1)
code2 := doPutManifestRequest(projectID, projectName, "photon", "latest", dgt)
suite.Equal(http.StatusCreated, code2)
}
func TestMain(m *testing.M) {
dao.PrepareTestForPostgresSQL()
if result := m.Run(); result != 0 {
os.Exit(result)
}
}
func TestRunHandlerSuite(t *testing.T) {
suite.Run(t, new(HandlerSuite))
}

View File

@ -1,15 +1,15 @@
package rule
import (
"github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/common/dao"
"github.com/goharbor/harbor/src/pkg/art"
"github.com/goharbor/harbor/src/pkg/immutabletag"
"github.com/goharbor/harbor/src/pkg/immutabletag/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"os"
"testing"
"time"
)
// MatchTestSuite ...
@ -25,7 +25,6 @@ type MatchTestSuite struct {
// SetupSuite ...
func (s *MatchTestSuite) SetupSuite() {
test.InitDatabaseFromEnv()
s.t = s.T()
s.assert = assert.New(s.t)
s.require = require.New(s.t)
@ -34,34 +33,31 @@ func (s *MatchTestSuite) SetupSuite() {
func (s *MatchTestSuite) TestImmuMatch() {
rule := &model.Metadata{
ID: 1,
ProjectID: 2,
ProjectID: 1,
Priority: 1,
Template: "latestPushedK",
Action: "immuablity",
Action: "immutable",
Template: "immutable_template",
TagSelectors: []*model.Selector{
{
Kind: "doublestar",
Decoration: "matches",
Pattern: "release-[\\d\\.]+",
Pattern: "release-**",
},
},
ScopeSelectors: map[string][]*model.Selector{
"repository": {
{
Kind: "doublestar",
Decoration: "matches",
Decoration: "repoMatches",
Pattern: "redis",
},
},
},
}
rule2 := &model.Metadata{
ID: 1,
ProjectID: 2,
ProjectID: 1,
Priority: 1,
Template: "latestPushedK",
Template: "immutable_template",
Action: "immuablity",
TagSelectors: []*model.Selector{
{
@ -74,7 +70,7 @@ func (s *MatchTestSuite) TestImmuMatch() {
"repository": {
{
Kind: "doublestar",
Decoration: "matches",
Decoration: "repoMatches",
Pattern: "mysql",
},
},
@ -83,69 +79,52 @@ func (s *MatchTestSuite) TestImmuMatch() {
id, err := s.ctr.CreateImmutableRule(rule)
s.ruleID = id
s.require.NotNil(err)
s.require.Nil(err)
id, err = s.ctr.CreateImmutableRule(rule2)
s.ruleID2 = id
s.require.NotNil(err)
s.require.Nil(err)
match := NewRuleMatcher(2)
match := NewRuleMatcher(1)
c1 := art.Candidate{
NamespaceID: 2,
Namespace: "immutable",
Repository: "redis",
Tag: "release-1.10",
Kind: art.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
Labels: []string{"label1", "label4", "label5"},
NamespaceID: 1,
Namespace: "library",
Repository: "redis",
Tag: "release-1.10",
}
isMatch, err := match.Match(c1)
s.require.Equal(isMatch, true)
s.require.Nil(err)
c2 := art.Candidate{
NamespaceID: 2,
Namespace: "immutable",
Repository: "redis",
Tag: "1.10",
Kind: art.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
Labels: []string{"label1", "label4", "label5"},
NamespaceID: 1,
Namespace: "library",
Repository: "redis",
Tag: "1.10",
Kind: art.Image,
}
isMatch, err = match.Match(c2)
s.require.Equal(isMatch, false)
s.require.Nil(err)
c3 := art.Candidate{
NamespaceID: 2,
Namespace: "immutable",
Repository: "mysql",
Tag: "9.4.8",
Kind: art.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
Labels: []string{"label1"},
NamespaceID: 1,
Namespace: "immutable",
Repository: "mysql",
Tag: "9.4.8",
Kind: art.Image,
}
isMatch, err = match.Match(c3)
s.require.Equal(isMatch, true)
s.require.Nil(err)
c4 := art.Candidate{
NamespaceID: 2,
Namespace: "immutable",
Repository: "hello",
Tag: "world",
Kind: art.Image,
PushedTime: time.Now().Unix() - 3600,
PulledTime: time.Now().Unix(),
CreationTime: time.Now().Unix() - 7200,
Labels: []string{"label1"},
NamespaceID: 1,
Namespace: "immutable",
Repository: "hello",
Tag: "world",
Kind: art.Image,
}
isMatch, err = match.Match(c4)
s.require.Equal(isMatch, false)
@ -160,3 +139,15 @@ func (s *MatchTestSuite) TearDownSuite() {
err = s.ctr.DeleteImmutableRule(s.ruleID2)
require.NoError(s.T(), err, "delete immutable")
}
func TestMain(m *testing.M) {
dao.PrepareTestForPostgresSQL()
if result := m.Run(); result != 0 {
os.Exit(result)
}
}
func TestRunHandlerSuite(t *testing.T) {
suite.Run(t, new(MatchTestSuite))
}