mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 04:48:26 +08:00
graphql: Apply authorization on /_search endpoint
- AccessControlContext now resides in a separate package from where it can be imported, along with the contextKey that will be used to set and retrieve this context value. - AccessControlContext has a new field called Username, that will be of use for future implementations in graphQL resolvers. - GlobalSearch resolver now uses this context to filter repos available to the logged user. - moved logic for uploading images in tests so that it can be used in every package - tests were added for multiple request scenarios, when zot-server requires authz on specific repos - added tests with injected errors for extended coverage - added tests for status code error injection utilities Closes https://github.com/project-zot/zot/issues/615 Signed-off-by: Alex Stan <alexandrustan96@yahoo.ro>
This commit is contained in:
@@ -4,17 +4,20 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
"github.com/opencontainers/image-spec/specs-go"
|
||||
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/opencontainers/umoci"
|
||||
"github.com/phayes/freeport"
|
||||
@@ -27,6 +30,18 @@ const (
|
||||
SleepTime = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPostBlob = errors.New("can't post blob")
|
||||
ErrPutBlob = errors.New("can't put blob")
|
||||
)
|
||||
|
||||
type Image struct {
|
||||
Manifest imagespec.Manifest
|
||||
Config imagespec.Image
|
||||
Layers [][]byte
|
||||
Tag string
|
||||
}
|
||||
|
||||
func GetFreePort() string {
|
||||
port, err := freeport.GetFreePort()
|
||||
if err != nil {
|
||||
@@ -264,3 +279,127 @@ func GetOciLayoutDigests(imagePath string) (godigest.Digest, godigest.Digest, go
|
||||
|
||||
return manifestDigest, configDigest, layerDigest
|
||||
}
|
||||
|
||||
func GetImageComponents(layerSize int) (imagespec.Image, [][]byte, imagespec.Manifest, error) {
|
||||
config := imagespec.Image{
|
||||
Architecture: "amd64",
|
||||
OS: "linux",
|
||||
RootFS: imagespec.RootFS{
|
||||
Type: "layers",
|
||||
DiffIDs: []godigest.Digest{},
|
||||
},
|
||||
Author: "ZotUser",
|
||||
}
|
||||
|
||||
configBlob, err := json.Marshal(config)
|
||||
if err = Error(err); err != nil {
|
||||
return imagespec.Image{}, [][]byte{}, imagespec.Manifest{}, err
|
||||
}
|
||||
|
||||
configDigest := godigest.FromBytes(configBlob)
|
||||
|
||||
layers := [][]byte{
|
||||
make([]byte, layerSize),
|
||||
}
|
||||
|
||||
schemaVersion := 2
|
||||
|
||||
manifest := imagespec.Manifest{
|
||||
Versioned: specs.Versioned{
|
||||
SchemaVersion: schemaVersion,
|
||||
},
|
||||
Config: imagespec.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.config.v1+json",
|
||||
Digest: configDigest,
|
||||
Size: int64(len(configBlob)),
|
||||
},
|
||||
Layers: []imagespec.Descriptor{
|
||||
{
|
||||
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
||||
Digest: godigest.FromBytes(layers[0]),
|
||||
Size: int64(len(layers[0])),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return config, layers, manifest, nil
|
||||
}
|
||||
|
||||
func UploadImage(img Image, baseURL, repo string) error {
|
||||
for _, blob := range img.Layers {
|
||||
resp, err := resty.R().Post(baseURL + "/v2/" + repo + "/blobs/uploads/")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode() != http.StatusAccepted {
|
||||
return ErrPostBlob
|
||||
}
|
||||
|
||||
loc := resp.Header().Get("Location")
|
||||
|
||||
digest := godigest.FromBytes(blob).String()
|
||||
|
||||
resp, err = resty.R().
|
||||
SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))).
|
||||
SetHeader("Content-Type", "application/octet-stream").
|
||||
SetQueryParam("digest", digest).
|
||||
SetBody(blob).
|
||||
Put(baseURL + loc)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode() != http.StatusCreated {
|
||||
return ErrPutBlob
|
||||
}
|
||||
}
|
||||
// upload config
|
||||
cblob, err := json.Marshal(img.Config)
|
||||
if err = Error(err); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cdigest := godigest.FromBytes(cblob)
|
||||
|
||||
resp, err := resty.R().
|
||||
Post(baseURL + "/v2/" + repo + "/blobs/uploads/")
|
||||
if err = Error(err); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ErrStatusCode(resp.StatusCode()) != http.StatusAccepted && ErrStatusCode(resp.StatusCode()) == -1 {
|
||||
return ErrPostBlob
|
||||
}
|
||||
|
||||
loc := Location(baseURL, resp)
|
||||
|
||||
// uploading blob should get 201
|
||||
resp, err = resty.R().
|
||||
SetHeader("Content-Length", fmt.Sprintf("%d", len(cblob))).
|
||||
SetHeader("Content-Type", "application/octet-stream").
|
||||
SetQueryParam("digest", cdigest.String()).
|
||||
SetBody(cblob).
|
||||
Put(loc)
|
||||
if err = Error(err); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ErrStatusCode(resp.StatusCode()) != http.StatusCreated && ErrStatusCode(resp.StatusCode()) == -1 {
|
||||
return ErrPostBlob
|
||||
}
|
||||
|
||||
// put manifest
|
||||
manifestBlob, err := json.Marshal(img.Manifest)
|
||||
if err = Error(err); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = resty.R().
|
||||
SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
|
||||
SetBody(manifestBlob).
|
||||
Put(baseURL + "/v2/" + repo + "/manifests/" + img.Tag)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,14 +4,18 @@
|
||||
package test_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"zotregistry.io/zot/pkg/api"
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/test"
|
||||
)
|
||||
|
||||
@@ -122,3 +126,283 @@ func TestGetOciLayoutDigests(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetImageComponents(t *testing.T) {
|
||||
Convey("Inject failures for unreachable lines", t, func() {
|
||||
injected := test.InjectFailure(0)
|
||||
if injected {
|
||||
_, _, _, err := test.GetImageComponents(100)
|
||||
So(err, ShouldNotBeNil)
|
||||
}
|
||||
})
|
||||
Convey("finishes successfully", t, func() {
|
||||
_, _, _, err := test.GetImageComponents(100)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUploadImage(t *testing.T) {
|
||||
Convey("Post request results in an error", t, func() {
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
conf.Storage.RootDirectory = t.TempDir()
|
||||
|
||||
img := test.Image{
|
||||
Layers: make([][]byte, 10),
|
||||
}
|
||||
|
||||
err := test.UploadImage(img, baseURL, "test")
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Post request status differs from accepted", t, func() {
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
tempDir := t.TempDir()
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
conf.Storage.RootDirectory = tempDir
|
||||
|
||||
err := os.Chmod(tempDir, 0o400)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
go startServer(ctlr)
|
||||
defer stopServer(ctlr)
|
||||
|
||||
test.WaitTillServerReady(baseURL)
|
||||
|
||||
img := test.Image{
|
||||
Layers: make([][]byte, 10),
|
||||
}
|
||||
|
||||
err = test.UploadImage(img, baseURL, "test")
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Put request results in an error", t, func() {
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
conf.Storage.RootDirectory = t.TempDir()
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
go startServer(ctlr)
|
||||
defer stopServer(ctlr)
|
||||
|
||||
test.WaitTillServerReady(baseURL)
|
||||
|
||||
img := test.Image{
|
||||
Layers: make([][]byte, 10), // invalid format that will result in an error
|
||||
Config: ispec.Image{},
|
||||
}
|
||||
|
||||
err := test.UploadImage(img, baseURL, "test")
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("Image uploaded successfully", t, func() {
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
conf.Storage.RootDirectory = t.TempDir()
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
go startServer(ctlr)
|
||||
defer stopServer(ctlr)
|
||||
|
||||
test.WaitTillServerReady(baseURL)
|
||||
|
||||
layerBlob := []byte("test")
|
||||
|
||||
img := test.Image{
|
||||
Layers: [][]byte{
|
||||
layerBlob,
|
||||
}, // invalid format that will result in an error
|
||||
Config: ispec.Image{},
|
||||
}
|
||||
|
||||
err := test.UploadImage(img, baseURL, "test")
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Blob upload wrong response status code", t, func() {
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
tempDir := t.TempDir()
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
conf.Storage.RootDirectory = tempDir
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
go startServer(ctlr)
|
||||
defer stopServer(ctlr)
|
||||
|
||||
test.WaitTillServerReady(baseURL)
|
||||
|
||||
layerBlob := []byte("test")
|
||||
layerBlobDigest := digest.FromBytes(layerBlob)
|
||||
layerPath := path.Join(tempDir, "test", "blobs", "sha256")
|
||||
|
||||
if _, err := os.Stat(layerPath); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(layerPath, 0o700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
file, err := os.Create(path.Join(layerPath, layerBlobDigest.Encoded()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = os.Chmod(layerPath, 0o000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
err = os.Chmod(layerPath, 0o700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.RemoveAll(file.Name())
|
||||
}()
|
||||
}
|
||||
|
||||
img := test.Image{
|
||||
Layers: [][]byte{
|
||||
layerBlob,
|
||||
}, // invalid format that will result in an error
|
||||
Config: ispec.Image{},
|
||||
}
|
||||
|
||||
err := test.UploadImage(img, baseURL, "test")
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("CreateBlobUpload wrong response status code", t, func() {
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
tempDir := t.TempDir()
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
conf.Storage.RootDirectory = tempDir
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
go startServer(ctlr)
|
||||
defer stopServer(ctlr)
|
||||
|
||||
test.WaitTillServerReady(baseURL)
|
||||
|
||||
layerBlob := []byte("test")
|
||||
|
||||
img := test.Image{
|
||||
Layers: [][]byte{
|
||||
layerBlob,
|
||||
}, // invalid format that will result in an error
|
||||
Config: ispec.Image{},
|
||||
}
|
||||
|
||||
Convey("CreateBlobUpload", func() {
|
||||
injected := test.InjectFailure(2)
|
||||
if injected {
|
||||
err := test.UploadImage(img, baseURL, "test")
|
||||
So(err, ShouldNotBeNil)
|
||||
}
|
||||
})
|
||||
Convey("UpdateBlobUpload", func() {
|
||||
injected := test.InjectFailure(4)
|
||||
if injected {
|
||||
err := test.UploadImage(img, baseURL, "test")
|
||||
So(err, ShouldNotBeNil)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestInjectUploadImage(t *testing.T) {
|
||||
Convey("Inject failures for unreachable lines", t, func() {
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
tempDir := t.TempDir()
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
conf.Storage.RootDirectory = tempDir
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
go startServer(ctlr)
|
||||
defer stopServer(ctlr)
|
||||
|
||||
test.WaitTillServerReady(baseURL)
|
||||
|
||||
layerBlob := []byte("test")
|
||||
layerPath := path.Join(tempDir, "test", ".uploads")
|
||||
|
||||
if _, err := os.Stat(layerPath); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(layerPath, 0o700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
img := test.Image{
|
||||
Layers: [][]byte{
|
||||
layerBlob,
|
||||
}, // invalid format that will result in an error
|
||||
Config: ispec.Image{},
|
||||
}
|
||||
|
||||
Convey("first marshal", func() {
|
||||
injected := test.InjectFailure(0)
|
||||
if injected {
|
||||
err := test.UploadImage(img, baseURL, "test")
|
||||
So(err, ShouldNotBeNil)
|
||||
}
|
||||
})
|
||||
Convey("CreateBlobUpload POST call", func() {
|
||||
injected := test.InjectFailure(1)
|
||||
if injected {
|
||||
err := test.UploadImage(img, baseURL, "test")
|
||||
So(err, ShouldNotBeNil)
|
||||
}
|
||||
})
|
||||
Convey("UpdateBlobUpload PUT call", func() {
|
||||
injected := test.InjectFailure(3)
|
||||
if injected {
|
||||
err := test.UploadImage(img, baseURL, "test")
|
||||
So(err, ShouldNotBeNil)
|
||||
}
|
||||
})
|
||||
Convey("second marshal", func() {
|
||||
injected := test.InjectFailure(5)
|
||||
if injected {
|
||||
err := test.UploadImage(img, baseURL, "test")
|
||||
So(err, ShouldNotBeNil)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func startServer(c *api.Controller) {
|
||||
// this blocks
|
||||
ctx := context.Background()
|
||||
if err := c.Run(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func stopServer(c *api.Controller) {
|
||||
ctx := context.Background()
|
||||
_ = c.Server.Shutdown(ctx)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
zerr "zotregistry.io/zot/errors"
|
||||
@@ -36,6 +37,20 @@ func Error(err error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Used to inject error status codes for coverage purposes.
|
||||
// -1 will be returned in case of successful failure injection.
|
||||
func ErrStatusCode(status int) int {
|
||||
if !injectedFailure() {
|
||||
if status == http.StatusAccepted || status == http.StatusCreated {
|
||||
return status
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Failure injection infrastructure to cover hard-to-reach code paths.
|
||||
|
||||
@@ -59,6 +59,18 @@ func bar() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func baz() error {
|
||||
if test.ErrStatusCode(0) != 0 {
|
||||
return errCall1
|
||||
}
|
||||
|
||||
if test.ErrStatusCode(0) != 0 {
|
||||
return errCall2
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func alwaysErr() error {
|
||||
return errNotZero
|
||||
}
|
||||
@@ -108,6 +120,22 @@ func TestInject(t *testing.T) {
|
||||
So(errors.Is(err, errCall2), ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Check ErrStatusCode", func() {
|
||||
Convey("Without skipping", func() {
|
||||
test.InjectFailure(0) // inject a failure
|
||||
err := baz() // should be a failure
|
||||
So(err, ShouldNotBeNil) // should be a failure
|
||||
So(errors.Is(err, errCall1), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("With skipping", func() {
|
||||
test.InjectFailure(1) // inject a failure but skip first one
|
||||
err := baz() // should be a failure
|
||||
So(errors.Is(err, errCall1), ShouldBeFalse)
|
||||
So(errors.Is(err, errCall2), ShouldBeTrue)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Without injected failure", t, func(c C) {
|
||||
|
||||
@@ -11,6 +11,10 @@ func Ok(ok bool) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
func ErrStatusCode(statusCode int) int {
|
||||
return statusCode
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Failure injection infrastructure to cover hard-to-reach code paths (nop in production).
|
||||
|
||||
Reference in New Issue
Block a user