Artifact signature populate (#7)

* Populate signature status in artifact API

This Commit add signature status into response of list artifact API.

Signed-off-by: Daniel Jiang <jiangd@vmware.com>
This commit is contained in:
Daniel Jiang 2020-02-18 01:42:56 +08:00 committed by GitHub
parent e5eb711827
commit 5a6e9331fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 254 additions and 34 deletions

View File

@ -563,6 +563,10 @@ definitions:
type: boolean
x-omitempty: false
description: The immutable status of the tag
signed:
type: boolean
x-omitempty: false
description: The attribute indicates whether the tag is signed or not
ExtraAttrs:
type: object
additionalProperties:

View File

@ -27,6 +27,7 @@ import (
"github.com/goharbor/harbor/src/pkg/immutabletag/match"
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
"github.com/goharbor/harbor/src/pkg/label"
"github.com/goharbor/harbor/src/pkg/signature"
"github.com/opencontainers/go-digest"
"strings"
@ -94,6 +95,7 @@ func NewController() Controller {
repoMgr: repository.Mgr,
artMgr: artifact.Mgr,
tagMgr: tag.Mgr,
sigMgr: signature.GetManager(),
labelMgr: label.Mgr,
abstractor: abstractor.NewAbstractor(),
immutableMtr: rule.NewRuleMatcher(),
@ -106,6 +108,7 @@ type controller struct {
repoMgr repository.Manager
artMgr artifact.Manager
tagMgr tag.Manager
sigMgr signature.Manager
labelMgr label.Manager
abstractor abstractor.Abstractor
immutableMtr match.ImmutableTagMatcher
@ -382,9 +385,6 @@ func (c *controller) assembleArtifact(ctx context.Context, art *artifact.Artifac
if option.WithScanOverview {
c.populateScanOverview(ctx, artifact)
}
if option.WithSignature {
c.populateSignature(ctx, artifact)
}
// populate addition links
c.populateAdditionLinks(ctx, artifact)
return artifact
@ -413,12 +413,36 @@ func (c *controller) assembleTag(ctx context.Context, tag *tm.Tag, option *TagOp
if option == nil {
return t
}
repo, err := c.repoMgr.Get(ctx, tag.RepositoryID)
if err != nil {
log.Errorf("Failed to get repo for tag: %s, error: %v", tag.Name, err)
return t
}
if option.WithImmutableStatus {
c.populateImmutableStatus(ctx, t)
}
if option.WithSignature {
if a, err := c.artMgr.Get(ctx, t.ArtifactID); err != nil {
log.Errorf("Failed to get artifact for tag: %s, error: %v, skip populating signature", t.Name, err)
} else {
c.populateTagSignature(ctx, repo.Name, t, a.Digest, option)
}
}
return t
}
func (c *controller) populateTagSignature(ctx context.Context, repo string, tag *Tag, digest string, option *TagOption) {
if option.SignatureChecker == nil {
chk, err := signature.GetManager().GetCheckerByRepo(ctx, repo)
if err != nil {
log.Error(err)
return
}
option.SignatureChecker = chk
}
tag.Signed = option.SignatureChecker.IsTagSigned(tag.Name, digest)
}
func (c *controller) populateLabels(ctx context.Context, art *Artifact) {
labels, err := c.labelMgr.ListByArtifact(ctx, art.ID)
if err != nil {

View File

@ -131,7 +131,6 @@ func (c *controllerTestSuite) TestAssembleArtifact() {
},
WithLabel: true,
WithScanOverview: true,
WithSignature: true,
}
tg := &tag.Tag{
ID: 1,
@ -262,7 +261,6 @@ func (c *controllerTestSuite) TestList() {
option := &Option{
WithTag: true,
WithScanOverview: true,
WithSignature: true,
}
c.artMgr.On("List").Return(1, []*artifact.Artifact{
{

View File

@ -18,6 +18,7 @@ import (
"github.com/go-openapi/strfmt"
cmodels "github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/pkg/artifact"
"github.com/goharbor/harbor/src/pkg/signature"
"github.com/goharbor/harbor/src/pkg/tag/model/tag"
"github.com/goharbor/harbor/src/server/v2.0/models"
)
@ -73,6 +74,7 @@ func (a *Artifact) ToSwagger() *models.Artifact {
PushTime: strfmt.DateTime(tag.PushTime),
RepositoryID: tag.RepositoryID,
Immutable: tag.Immutable,
Signed: tag.Signed,
})
}
for addition, link := range a.AdditionLinks {
@ -104,7 +106,8 @@ func (a *Artifact) ToSwagger() *models.Artifact {
type Tag struct {
tag.Tag
Immutable bool
// TODO add other attrs: signature, etc
Signed bool
// TODO add other attrs: label, etc
}
// AdditionLink is a link via that the addition can be fetched
@ -119,13 +122,13 @@ type Option struct {
TagOption *TagOption // only works when WithTag is set to true
WithLabel bool
WithScanOverview bool
// TODO move it to TagOption?
WithSignature bool
}
// TagOption is used to specify the properties returned when listing/getting tags
type TagOption struct {
WithImmutableStatus bool
WithSignature bool
SignatureChecker *signature.Checker
}
// TODO move this to GC controller?

View File

@ -17,7 +17,7 @@ package models
import (
"time"
"github.com/goharbor/harbor/src/common/utils/notary/model"
"github.com/goharbor/harbor/src/pkg/signature/notary/model"
"github.com/theupdateframework/notary/tuf/data"
)

View File

@ -34,8 +34,6 @@ import (
"github.com/goharbor/harbor/src/common/rbac"
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/common/utils/notary"
notarymodel "github.com/goharbor/harbor/src/common/utils/notary/model"
"github.com/goharbor/harbor/src/common/utils/registry"
"github.com/goharbor/harbor/src/core/config"
notifierEvt "github.com/goharbor/harbor/src/core/notifier/event"
@ -45,6 +43,8 @@ import (
"github.com/goharbor/harbor/src/pkg/immutabletag/match/rule"
"github.com/goharbor/harbor/src/pkg/scan/api/scan"
v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
"github.com/goharbor/harbor/src/pkg/signature/notary"
notarymodel "github.com/goharbor/harbor/src/pkg/signature/notary/model"
"github.com/goharbor/harbor/src/replication"
"github.com/goharbor/harbor/src/replication/event"
"github.com/goharbor/harbor/src/replication/model"

View File

@ -17,9 +17,9 @@ package contenttrust
import (
"github.com/goharbor/harbor/src/common/utils"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/common/utils/notary"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/core/middlewares/util"
"github.com/goharbor/harbor/src/pkg/signature/notary"
"net/http"
"net/http/httptest"
)

View File

@ -20,9 +20,9 @@ import (
"testing"
"github.com/goharbor/harbor/src/common"
notarytest "github.com/goharbor/harbor/src/common/utils/notary/test"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/core/middlewares/util"
notarytest "github.com/goharbor/harbor/src/pkg/signature/notary/test"
"github.com/stretchr/testify/assert"
)

View File

@ -29,10 +29,10 @@ import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/common/models"
"github.com/goharbor/harbor/src/common/utils"
notarytest "github.com/goharbor/harbor/src/common/utils/notary/test"
testutils "github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/pkg/scan/vuln"
notarytest "github.com/goharbor/harbor/src/pkg/signature/notary/test"
digest "github.com/opencontainers/go-digest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

View File

@ -0,0 +1,94 @@
// 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 signature
import (
"github.com/goharbor/harbor/src/common/security"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/pkg/signature/notary"
"github.com/goharbor/harbor/src/pkg/signature/notary/model"
"golang.org/x/net/context"
)
// Checker checks the signature status of artifact
type Checker struct {
signatures map[string]string
}
// IsTagSigned checks if the tag of the artifact is signed, it also checks the signed artifact has the same digest as parm.
func (sc Checker) IsTagSigned(tag, digest string) bool {
d, ok := sc.signatures[tag]
if len(digest) == 0 {
return ok
}
return digest == d
}
// IsArtifactSigned checks if the artifact with given digest is signed.
func (sc Checker) IsArtifactSigned(digest string) bool {
for _, v := range sc.signatures {
if v == digest {
return true
}
}
return false
}
// Manager interface for handling signatures of artifacts
type Manager interface {
// GetCheckerByRepo returns a Checker for checking signature
GetCheckerByRepo(ctx context.Context, repo string) (*Checker, error)
}
type mgr struct {
}
// GetCheckerByRepo ...
func (m *mgr) GetCheckerByRepo(ctx context.Context, repo string) (*Checker, error) {
if !config.WithNotary() { // return a checker that always return false
return &Checker{}, nil
}
s := make(map[string]string)
targets, err := m.getTargetsByRepo(ctx, repo)
if err != nil {
return nil, err
}
for _, t := range targets {
if d, err := notary.DigestFromTarget(t); err != nil {
log.Warningf("Failed to get signed digest for tag %s, error: %v, skip", t.Tag, err)
} else {
s[t.Tag] = d
}
}
return &Checker{s}, nil
}
func (m *mgr) getTargetsByRepo(ctx context.Context, repo string) ([]model.Target, error) {
name := "unknown"
if sc, ok := security.FromContext(ctx); !ok || sc == nil {
log.Warningf("Unable to get security context")
} else {
name = sc.GetUsername()
}
return notary.GetInternalTargets(config.InternalNotaryEndpoint(), name, repo)
}
var instance = &mgr{}
// GetManager ...
func GetManager() Manager {
return instance
}

View File

@ -0,0 +1,100 @@
package signature
import (
"github.com/goharbor/harbor/src/common"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/pkg/signature/notary/test"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
"os"
"testing"
)
func TestMain(m *testing.M) {
// B/C the notary requires private key for signing token, b
// before running locally, please make sure the env var is set as follow:
// export TOKEN_PRIVATE_KEY_PATH="/harbor/tests/private_key.pem"
endpoint := "10.117.4.142"
// notary-demo/busybox:1.0 is signed, more details in the notary/test pkg
notaryServer := test.NewNotaryServer(endpoint)
defer notaryServer.Close()
conf := map[string]interface{}{
common.WithNotary: "true",
common.NotaryURL: notaryServer.URL,
common.ExtEndpoint: "https://" + endpoint,
}
config.InitWithSettings(conf)
result := m.Run()
if result != 0 {
os.Exit(result)
}
}
func TestGetCheckerByRepo(t *testing.T) {
type in struct {
repo string
tag string
digest string
}
type res struct {
tagSigned bool
artSigned bool
}
m := GetManager()
cases := []struct {
input in
expect res
}{
{
input: in{
repo: "notary-demo/busybox",
tag: "1.0",
digest: "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7",
},
expect: res{
tagSigned: true,
artSigned: true,
},
},
{
input: in{
repo: "notary-demo/busybox",
tag: "1.0",
digest: "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a8",
},
expect: res{
tagSigned: false,
artSigned: false,
},
},
{
input: in{
repo: "notary-demo/busybox",
tag: "2.0",
digest: "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7",
},
expect: res{
tagSigned: false,
artSigned: true,
},
},
{
input: in{
repo: "non-exist",
tag: "1.0",
digest: "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7",
},
expect: res{
tagSigned: false,
artSigned: false,
},
},
}
for _, c := range cases {
checker, err := m.GetCheckerByRepo(context.Background(), c.input.repo)
assert.Nil(t, err)
assert.Equal(t, c.expect.tagSigned, checker.IsTagSigned(c.input.tag, c.input.digest),
"Unexpected tagSigned value for input: %#v", c.input)
assert.Equal(t, c.expect.artSigned, checker.IsArtifactSigned(c.input.digest), "Unexpected artSigned value for input: %#v", c.input)
}
}

View File

@ -17,13 +17,12 @@ package notary
import (
"encoding/hex"
"fmt"
model2 "github.com/goharbor/harbor/src/pkg/signature/notary/model"
"net/http"
"os"
"path"
"strings"
"github.com/goharbor/harbor/src/common/utils/notary/model"
"github.com/docker/distribution/registry/auth/token"
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/common/utils/registry"
@ -54,7 +53,7 @@ func init() {
}
// GetInternalTargets wraps GetTargets to read config values for getting full-qualified repo from internal notary instance.
func GetInternalTargets(notaryEndpoint string, username string, repo string) ([]model.Target, error) {
func GetInternalTargets(notaryEndpoint string, username string, repo string) ([]model2.Target, error) {
ext, err := config.ExtEndpoint()
if err != nil {
log.Errorf("Error while reading external endpoint: %v", err)
@ -68,8 +67,8 @@ func GetInternalTargets(notaryEndpoint string, username string, repo string) ([]
// GetTargets is a help function called by API to fetch signature information of a given repository.
// Per docker's convention the repository should contain the information of endpoint, i.e. it should look
// like "192.168.0.1/library/ubuntu", instead of "library/ubuntu" (fqRepo for fully-qualified repo)
func GetTargets(notaryEndpoint string, username string, fqRepo string) ([]model.Target, error) {
res := []model.Target{}
func GetTargets(notaryEndpoint string, username string, fqRepo string) ([]model2.Target, error) {
res := []model2.Target{}
t, err := tokenutil.MakeToken(username, tokenutil.Notary,
[]*token.ResourceActions{
{
@ -103,7 +102,7 @@ func GetTargets(notaryEndpoint string, username string, fqRepo string) ([]model.
log.Warningf("Failed to clear cached root.json: %s, error: %v, when repo is removed from notary the signature status maybe incorrect", rootJSON, rmErr)
}
for _, t := range targets {
res = append(res, model.Target{
res = append(res, model2.Target{
Tag: t.Name,
Hashes: t.Hashes,
})
@ -112,7 +111,7 @@ func GetTargets(notaryEndpoint string, username string, fqRepo string) ([]model.
}
// DigestFromTarget get a target and return the value of digest, in accordance to Docker-Content-Digest
func DigestFromTarget(t model.Target) (string, error) {
func DigestFromTarget(t model2.Target) (string, error) {
sha, ok := t.Hashes["sha256"]
if !ok {
return "", fmt.Errorf("no valid hash, expecting sha256")

View File

@ -16,10 +16,9 @@ package notary
import (
"encoding/json"
"fmt"
model2 "github.com/goharbor/harbor/src/pkg/signature/notary/model"
test2 "github.com/goharbor/harbor/src/pkg/signature/notary/test"
"github.com/goharbor/harbor/src/common/utils/notary/model"
notarytest "github.com/goharbor/harbor/src/common/utils/notary/test"
"github.com/goharbor/harbor/src/common/utils/test"
"github.com/goharbor/harbor/src/core/config"
"github.com/stretchr/testify/assert"
@ -36,7 +35,7 @@ var endpoint = "10.117.4.142"
var notaryServer *httptest.Server
func TestMain(m *testing.M) {
notaryServer = notarytest.NewNotaryServer(endpoint)
notaryServer = test2.NewNotaryServer(endpoint)
defer notaryServer.Close()
var defaultConfig = map[string]interface{}{
common.ExtEndpoint: "https://" + endpoint,
@ -80,13 +79,13 @@ func TestGetDigestFromTarget(t *testing.T) {
}
}`
var t1 model.Target
var t1 model2.Target
err := json.Unmarshal([]byte(str), &t1)
if err != nil {
panic(err)
}
hash2 := make(map[string][]byte)
t2 := model.Target{
t2 := model2.Target{
Tag: "2.0",
Hashes: hash2,
}

View File

@ -2,10 +2,10 @@ package contenttrust
import (
"github.com/goharbor/harbor/src/common/utils/log"
"github.com/goharbor/harbor/src/common/utils/notary"
"github.com/goharbor/harbor/src/core/config"
"github.com/goharbor/harbor/src/core/middlewares/util"
internal_errors "github.com/goharbor/harbor/src/internal/error"
"github.com/goharbor/harbor/src/pkg/signature/notary"
serror "github.com/goharbor/harbor/src/server/error"
"github.com/goharbor/harbor/src/server/middleware"
"net/http"

View File

@ -229,10 +229,12 @@ func option(withTag, withImmutableStatus, withLabel, withScanOverview, withSigna
option.WithTag = *(withTag)
}
if option.WithTag {
option.TagOption = &artifact.TagOption{}
if withImmutableStatus != nil {
option.TagOption = &artifact.TagOption{
WithImmutableStatus: *(withImmutableStatus),
}
option.TagOption.WithImmutableStatus = *(withImmutableStatus)
}
if withSignature != nil {
option.TagOption.WithSignature = *withSignature
}
}
if withLabel != nil {
@ -241,8 +243,5 @@ func option(withTag, withImmutableStatus, withLabel, withScanOverview, withSigna
if withScanOverview != nil {
option.WithScanOverview = *(withScanOverview)
}
if withSignature != nil {
option.WithSignature = *(withSignature)
}
return option
}