mirror of
https://github.com/project-zot/zot.git
synced 2024-12-23 22:27:35 -05:00
c6b822f3dd
fix(authz): fix isAdmin not using groups to determine if a user is admin. fix(authz): return 401 instead of 403 403 is correct as per HTTP spec However authz is not part of dist-spec and clients know only about 401 So this is a compromise. Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
9496 lines
297 KiB
Go
9496 lines
297 KiB
Go
//go:build sync && scrub && metrics && search && lint && userprefs && mgmt && imagetrust && ui
|
|
// +build sync,scrub,metrics,search,lint,userprefs,mgmt,imagetrust,ui
|
|
|
|
package api_test
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
goerrors "errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-github/v52/github"
|
|
"github.com/gorilla/mux"
|
|
"github.com/gorilla/securecookie"
|
|
"github.com/gorilla/sessions"
|
|
"github.com/migueleliasweb/go-github-mock/src/mock"
|
|
vldap "github.com/nmcclain/ldap"
|
|
notreg "github.com/notaryproject/notation-go/registry"
|
|
distext "github.com/opencontainers/distribution-spec/specs-go/v1/extensions"
|
|
godigest "github.com/opencontainers/go-digest"
|
|
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1"
|
|
"github.com/project-zot/mockoidc"
|
|
"github.com/sigstore/cosign/v2/cmd/cosign/cli/generate"
|
|
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
|
|
"github.com/sigstore/cosign/v2/cmd/cosign/cli/sign"
|
|
"github.com/sigstore/cosign/v2/cmd/cosign/cli/verify"
|
|
"github.com/sigstore/cosign/v2/pkg/oci/remote"
|
|
. "github.com/smartystreets/goconvey/convey"
|
|
"github.com/stretchr/testify/assert"
|
|
"go.etcd.io/bbolt"
|
|
"golang.org/x/crypto/bcrypt"
|
|
"gopkg.in/resty.v1"
|
|
|
|
"zotregistry.io/zot/errors"
|
|
"zotregistry.io/zot/pkg/api"
|
|
"zotregistry.io/zot/pkg/api/config"
|
|
"zotregistry.io/zot/pkg/api/constants"
|
|
apiErr "zotregistry.io/zot/pkg/api/errors"
|
|
"zotregistry.io/zot/pkg/common"
|
|
extconf "zotregistry.io/zot/pkg/extensions/config"
|
|
"zotregistry.io/zot/pkg/log"
|
|
"zotregistry.io/zot/pkg/meta"
|
|
"zotregistry.io/zot/pkg/storage"
|
|
storageConstants "zotregistry.io/zot/pkg/storage/constants"
|
|
"zotregistry.io/zot/pkg/test"
|
|
"zotregistry.io/zot/pkg/test/inject"
|
|
"zotregistry.io/zot/pkg/test/mocks"
|
|
)
|
|
|
|
const (
|
|
username = "test"
|
|
htpasswdUsername = "htpasswduser"
|
|
passphrase = "test"
|
|
group = "test"
|
|
repo = "test"
|
|
ServerCert = "../../test/data/server.cert"
|
|
ServerKey = "../../test/data/server.key"
|
|
CACert = "../../test/data/ca.crt"
|
|
AuthorizedNamespace = "everyone/isallowed"
|
|
UnauthorizedNamespace = "fortknox/notallowed"
|
|
ALICE = "alice"
|
|
AuthorizationNamespace = "authz/image"
|
|
AuthorizationAllRepos = "**"
|
|
)
|
|
|
|
func getCredString(username, password string) string {
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
usernameAndHash := fmt.Sprintf("%s:%s", username, string(hash))
|
|
|
|
return usernameAndHash
|
|
}
|
|
|
|
func skipIt(t *testing.T) {
|
|
t.Helper()
|
|
|
|
if os.Getenv("S3MOCK_ENDPOINT") == "" {
|
|
t.Skip("Skipping testing without AWS S3 mock server")
|
|
}
|
|
}
|
|
|
|
func skipDynamo(t *testing.T) {
|
|
t.Helper()
|
|
|
|
if os.Getenv("DYNAMODBMOCK_ENDPOINT") == "" {
|
|
t.Skip("Skipping testing without AWS DynamoDB mock server")
|
|
}
|
|
}
|
|
|
|
func TestNew(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
conf := config.New()
|
|
So(conf, ShouldNotBeNil)
|
|
So(api.NewController(conf), ShouldNotBeNil)
|
|
})
|
|
}
|
|
|
|
func TestCreateCacheDatabaseDriver(t *testing.T) {
|
|
Convey("Test CreateCacheDatabaseDriver boltdb", t, func() {
|
|
log := log.NewLogger("debug", "")
|
|
|
|
// fail create db, no perm
|
|
dir := t.TempDir()
|
|
conf := config.New()
|
|
conf.Storage.RootDirectory = dir
|
|
conf.Storage.Dedupe = true
|
|
conf.Storage.RemoteCache = false
|
|
|
|
err := os.Chmod(dir, 0o000)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
driver := storage.CreateCacheDatabaseDriver(conf.Storage.StorageConfig, log)
|
|
So(driver, ShouldBeNil)
|
|
|
|
conf.Storage.RemoteCache = true
|
|
conf.Storage.RootDirectory = t.TempDir()
|
|
|
|
driver = storage.CreateCacheDatabaseDriver(conf.Storage.StorageConfig, log)
|
|
So(driver, ShouldBeNil)
|
|
})
|
|
skipDynamo(t)
|
|
skipIt(t)
|
|
Convey("Test CreateCacheDatabaseDriver dynamodb", t, func() {
|
|
log := log.NewLogger("debug", "")
|
|
dir := t.TempDir()
|
|
// good config
|
|
conf := config.New()
|
|
conf.Storage.RootDirectory = dir
|
|
conf.Storage.Dedupe = true
|
|
conf.Storage.RemoteCache = true
|
|
conf.Storage.StorageDriver = map[string]interface{}{
|
|
"name": "s3",
|
|
"rootdirectory": "/zot",
|
|
"region": "us-east-2",
|
|
"bucket": "zot-storage",
|
|
"secure": true,
|
|
"skipverify": false,
|
|
}
|
|
|
|
conf.Storage.CacheDriver = map[string]interface{}{
|
|
"name": "dynamodb",
|
|
"endpoint": "http://localhost:4566",
|
|
"region": "us-east-2",
|
|
"cacheTablename": "BlobTable",
|
|
"repoMetaTablename": "RepoMetadataTable",
|
|
"manifestDataTablename": "ManifestDataTable",
|
|
"userDataTablename": "ZotUserDataTable",
|
|
"versionTablename": "Version",
|
|
}
|
|
|
|
driver := storage.CreateCacheDatabaseDriver(conf.Storage.StorageConfig, log)
|
|
So(driver, ShouldNotBeNil)
|
|
|
|
// negative test cases
|
|
|
|
conf.Storage.CacheDriver = map[string]interface{}{
|
|
"endpoint": "http://localhost:4566",
|
|
"region": "us-east-2",
|
|
"cacheTablename": "BlobTable",
|
|
"repoMetaTablename": "RepoMetadataTable",
|
|
"manifestDataTablename": "ManifestDataTable",
|
|
"userDataTablename": "ZotUserDataTable",
|
|
"versionTablename": "Version",
|
|
}
|
|
|
|
driver = storage.CreateCacheDatabaseDriver(conf.Storage.StorageConfig, log)
|
|
So(driver, ShouldBeNil)
|
|
|
|
conf.Storage.CacheDriver = map[string]interface{}{
|
|
"name": "dummy",
|
|
"endpoint": "http://localhost:4566",
|
|
"region": "us-east-2",
|
|
"cacheTablename": "BlobTable",
|
|
"repoMetaTablename": "RepoMetadataTable",
|
|
"manifestDataTablename": "ManifestDataTable",
|
|
"userDataTablename": "ZotUserDataTable",
|
|
"versionTablename": "Version",
|
|
}
|
|
|
|
driver = storage.CreateCacheDatabaseDriver(conf.Storage.StorageConfig, log)
|
|
So(driver, ShouldBeNil)
|
|
})
|
|
}
|
|
|
|
func TestCreateMetaDBDriver(t *testing.T) {
|
|
Convey("Test CreateCacheDatabaseDriver dynamo", t, func() {
|
|
log := log.NewLogger("debug", "")
|
|
dir := t.TempDir()
|
|
conf := config.New()
|
|
conf.Storage.RootDirectory = dir
|
|
conf.Storage.Dedupe = true
|
|
conf.Storage.RemoteCache = true
|
|
conf.Storage.StorageDriver = map[string]interface{}{
|
|
"name": "s3",
|
|
"rootdirectory": "/zot",
|
|
"region": "us-east-2",
|
|
"bucket": "zot-storage",
|
|
"secure": true,
|
|
"skipverify": false,
|
|
}
|
|
|
|
conf.Storage.CacheDriver = map[string]interface{}{
|
|
"name": "dummy",
|
|
"endpoint": "http://localhost:4566",
|
|
"region": "us-east-2",
|
|
"cachetablename": "BlobTable",
|
|
"repometatablename": "RepoMetadataTable",
|
|
"manifestdatatablename": "ManifestDataTable",
|
|
"userdatatablename": "UserDatatable",
|
|
}
|
|
|
|
testFunc := func() { _, _ = meta.New(conf.Storage.StorageConfig, log) }
|
|
So(testFunc, ShouldPanic)
|
|
|
|
conf.Storage.CacheDriver = map[string]interface{}{
|
|
"name": "dummy",
|
|
"endpoint": "http://localhost:4566",
|
|
"region": "us-east-2",
|
|
"cachetablename": "",
|
|
"repometatablename": "RepoMetadataTable",
|
|
"manifestdatatablename": "ManifestDataTable",
|
|
"userDataTablename": "ZotUserDataTable",
|
|
"versiontablename": 1,
|
|
}
|
|
|
|
testFunc = func() { _, _ = meta.New(conf.Storage.StorageConfig, log) }
|
|
So(testFunc, ShouldPanic)
|
|
|
|
conf.Storage.CacheDriver = map[string]interface{}{
|
|
"name": "dummy",
|
|
"endpoint": "http://localhost:4566",
|
|
"region": "us-east-2",
|
|
"cachetablename": "test",
|
|
"repometatablename": "RepoMetadataTable",
|
|
"manifestdatatablename": "ManifestDataTable",
|
|
"indexdatatablename": "IndexDataTable",
|
|
"userdatatablename": "ZotUserDataTable",
|
|
"apikeytablename": "APIKeyTable",
|
|
"versiontablename": "1",
|
|
}
|
|
|
|
testFunc = func() { _, _ = meta.New(conf.Storage.StorageConfig, log) }
|
|
So(testFunc, ShouldNotPanic)
|
|
})
|
|
|
|
Convey("Test CreateCacheDatabaseDriver bolt", t, func() {
|
|
log := log.NewLogger("debug", "")
|
|
dir := t.TempDir()
|
|
conf := config.New()
|
|
conf.Storage.RootDirectory = dir
|
|
conf.Storage.Dedupe = true
|
|
conf.Storage.RemoteCache = false
|
|
|
|
const perms = 0o600
|
|
|
|
boltDB, err := bbolt.Open(path.Join(dir, "repo.db"), perms, &bbolt.Options{Timeout: time.Second * 10})
|
|
So(err, ShouldBeNil)
|
|
|
|
err = boltDB.Close()
|
|
So(err, ShouldBeNil)
|
|
|
|
err = os.Chmod(path.Join(dir, "repo.db"), 0o200)
|
|
So(err, ShouldBeNil)
|
|
|
|
_, err = meta.New(conf.Storage.StorageConfig, log)
|
|
So(err, ShouldNotBeNil)
|
|
|
|
err = os.Chmod(path.Join(dir, "repo.db"), 0o600)
|
|
So(err, ShouldBeNil)
|
|
|
|
defer os.Remove(path.Join(dir, "repo.db"))
|
|
})
|
|
}
|
|
|
|
func TestRunAlreadyRunningServer(t *testing.T) {
|
|
Convey("Run server on unavailable port", t, func() {
|
|
port := test.GetFreePort()
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
cm := test.NewControllerManager(ctlr)
|
|
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
err := ctlr.Init(context.Background())
|
|
So(err, ShouldBeNil)
|
|
|
|
err = ctlr.Run(context.Background())
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
}
|
|
|
|
func TestAutoPortSelection(t *testing.T) {
|
|
Convey("Run server with specifying a port", t, func() {
|
|
conf := config.New()
|
|
conf.HTTP.Port = "0"
|
|
|
|
logFile, err := os.CreateTemp("", "zot-log*.txt")
|
|
So(err, ShouldBeNil)
|
|
conf.Log.Output = logFile.Name()
|
|
defer os.Remove(logFile.Name()) // clean up
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartServer()
|
|
time.Sleep(1000 * time.Millisecond)
|
|
defer cm.StopServer()
|
|
|
|
file, err := os.Open(logFile.Name())
|
|
So(err, ShouldBeNil)
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
|
|
var contents bytes.Buffer
|
|
start := time.Now()
|
|
|
|
for scanner.Scan() {
|
|
if time.Since(start) < time.Second*30 {
|
|
t.Logf("Exhausted: Controller did not print the expected log within 30 seconds")
|
|
}
|
|
text := scanner.Text()
|
|
contents.WriteString(text)
|
|
if strings.Contains(text, "Port unspecified") {
|
|
break
|
|
}
|
|
t.Logf(scanner.Text())
|
|
}
|
|
So(scanner.Err(), ShouldBeNil)
|
|
So(contents.String(), ShouldContainSubstring,
|
|
"port is unspecified, listening on kernel chosen port",
|
|
)
|
|
So(contents.String(), ShouldContainSubstring, "\"address\":\"127.0.0.1\"")
|
|
So(contents.String(), ShouldContainSubstring, "\"port\":")
|
|
|
|
So(ctlr.GetPort(), ShouldBeGreaterThan, 0)
|
|
So(ctlr.GetPort(), ShouldBeLessThan, 65536)
|
|
})
|
|
}
|
|
|
|
func TestObjectStorageController(t *testing.T) {
|
|
skipIt(t)
|
|
|
|
bucket := "zot-storage-test"
|
|
|
|
Convey("Negative make a new object storage controller", t, func() {
|
|
port := test.GetFreePort()
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
storageDriverParams := map[string]interface{}{
|
|
"rootdirectory": "zot",
|
|
"name": storageConstants.S3StorageDriverName,
|
|
}
|
|
conf.Storage.StorageDriver = storageDriverParams
|
|
ctlr := makeController(conf, "zot")
|
|
So(ctlr, ShouldNotBeNil)
|
|
|
|
err := ctlr.Init(context.Background())
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Make a new object storage controller", t, func() {
|
|
port := test.GetFreePort()
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
endpoint := os.Getenv("S3MOCK_ENDPOINT")
|
|
|
|
storageDriverParams := map[string]interface{}{
|
|
"rootdirectory": "zot",
|
|
"name": storageConstants.S3StorageDriverName,
|
|
"region": "us-east-2",
|
|
"bucket": bucket,
|
|
"regionendpoint": endpoint,
|
|
"secure": false,
|
|
"skipverify": false,
|
|
}
|
|
|
|
conf.Storage.StorageDriver = storageDriverParams
|
|
ctlr := makeController(conf, "/")
|
|
So(ctlr, ShouldNotBeNil)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
})
|
|
|
|
Convey("Make a new object storage controller with openid", t, func() {
|
|
port := test.GetFreePort()
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
endpoint := os.Getenv("S3MOCK_ENDPOINT")
|
|
|
|
storageDriverParams := map[string]interface{}{
|
|
"rootdirectory": "/zot",
|
|
"name": storageConstants.S3StorageDriverName,
|
|
"region": "us-east-2",
|
|
"bucket": bucket,
|
|
"regionendpoint": endpoint,
|
|
"secure": false,
|
|
"skipverify": false,
|
|
}
|
|
conf.Storage.RemoteCache = true
|
|
conf.Storage.StorageDriver = storageDriverParams
|
|
|
|
conf.Storage.CacheDriver = map[string]interface{}{
|
|
"name": "dynamodb",
|
|
"endpoint": "http://localhost:4566",
|
|
"region": "us-east-2",
|
|
"cachetablename": "test",
|
|
"repometatablename": "RepoMetadataTable",
|
|
"manifestdatatablename": "ManifestDataTable",
|
|
"indexdatatablename": "IndexDataTable",
|
|
"userdatatablename": "ZotUserDataTable",
|
|
"apikeytablename": "APIKeyTable1",
|
|
"versiontablename": "Version",
|
|
}
|
|
|
|
mockOIDCServer, err := test.MockOIDCRun()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
defer func() {
|
|
err := mockOIDCServer.Shutdown()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
|
|
mockOIDCConfig := mockOIDCServer.Config()
|
|
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
OpenID: &config.OpenIDConfig{
|
|
Providers: map[string]config.OpenIDProviderConfig{
|
|
"oidc": {
|
|
ClientID: mockOIDCConfig.ClientID,
|
|
ClientSecret: mockOIDCConfig.ClientSecret,
|
|
KeyPath: "",
|
|
Issuer: mockOIDCConfig.Issuer,
|
|
Scopes: []string{"openid", "email"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// create s3 bucket
|
|
_, err = resty.R().Put("http://" + os.Getenv("S3MOCK_ENDPOINT") + "/" + bucket)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ctlr := makeController(conf, "/")
|
|
So(ctlr, ShouldNotBeNil)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
})
|
|
}
|
|
|
|
func TestObjectStorageControllerSubPaths(t *testing.T) {
|
|
skipIt(t)
|
|
|
|
bucket := "zot-storage-test"
|
|
|
|
Convey("Make a new object storage controller", t, func() {
|
|
port := test.GetFreePort()
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
endpoint := os.Getenv("S3MOCK_ENDPOINT")
|
|
|
|
storageDriverParams := map[string]interface{}{
|
|
"rootdirectory": "zot",
|
|
"name": storageConstants.S3StorageDriverName,
|
|
"region": "us-east-2",
|
|
"bucket": bucket,
|
|
"regionendpoint": endpoint,
|
|
"secure": false,
|
|
"skipverify": false,
|
|
}
|
|
conf.Storage.StorageDriver = storageDriverParams
|
|
ctlr := makeController(conf, "zot")
|
|
So(ctlr, ShouldNotBeNil)
|
|
|
|
subPathMap := make(map[string]config.StorageConfig)
|
|
subPathMap["/a"] = config.StorageConfig{
|
|
RootDirectory: "/a",
|
|
StorageDriver: storageDriverParams,
|
|
}
|
|
ctlr.Config.Storage.SubPaths = subPathMap
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
})
|
|
}
|
|
|
|
func TestHtpasswdSingleCred(t *testing.T) {
|
|
Convey("Single cred", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
singleCredtests := []string{}
|
|
user := ALICE
|
|
password := ALICE
|
|
singleCredtests = append(singleCredtests, getCredString(user, password))
|
|
singleCredtests = append(singleCredtests, getCredString(user, password)+"\n")
|
|
|
|
for _, testString := range singleCredtests {
|
|
func() {
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(testString)
|
|
defer os.Remove(htpasswdPath)
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
|
|
conf.HTTP.AllowOrigin = conf.HTTP.Address
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// with creds, should get expected status code
|
|
resp, _ := resty.R().SetBasicAuth(user, password).Get(baseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
header := []string{"Authorization,content-type," + constants.SessionClientHeaderName}
|
|
|
|
resp, _ = resty.R().SetBasicAuth(user, password).Options(baseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
|
|
So(len(resp.Header()), ShouldEqual, 5)
|
|
So(resp.Header()["Access-Control-Allow-Headers"], ShouldResemble, header)
|
|
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
|
|
|
|
// with invalid creds, it should fail
|
|
resp, _ = resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
}()
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAllowMethodsHeader(t *testing.T) {
|
|
Convey("Options request", t, func() {
|
|
dir := t.TempDir()
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
conf.Storage.RootDirectory = dir
|
|
conf.HTTP.AllowOrigin = "someOrigin"
|
|
|
|
simpleUser := "simpleUser"
|
|
simpleUserPassword := "simpleUserPass"
|
|
credTests := fmt.Sprintf("%s\n\n", getCredString(simpleUser, simpleUserPassword))
|
|
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(credTests)
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Repositories: config.Repositories{
|
|
"**": config.PolicyGroup{
|
|
Policies: []config.Policy{
|
|
{
|
|
Users: []string{simpleUser},
|
|
Actions: []string{"read", "create"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
defaultVal := true
|
|
conf.Extensions = &extconf.ExtensionConfig{
|
|
Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
|
|
}
|
|
|
|
ctlr := api.NewController(conf)
|
|
|
|
ctlrManager := test.NewControllerManager(ctlr)
|
|
ctlrManager.StartAndWait(port)
|
|
defer ctlrManager.StopServer()
|
|
|
|
simpleUserClient := resty.R().SetBasicAuth(simpleUser, simpleUserPassword)
|
|
|
|
digest := godigest.FromString("digest")
|
|
|
|
// /v2
|
|
resp, err := simpleUserClient.Options(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
|
|
|
|
// /v2/{name}/tags/list
|
|
resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/tags/list")
|
|
So(err, ShouldBeNil)
|
|
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
|
|
|
|
// /v2/{name}/manifests/{reference}
|
|
resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/manifests/" + digest.String())
|
|
So(err, ShouldBeNil)
|
|
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,OPTIONS")
|
|
|
|
// /v2/{name}/referrers/{digest}
|
|
resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/referrers/" + digest.String())
|
|
So(err, ShouldBeNil)
|
|
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
|
|
|
|
// /v2/_catalog
|
|
resp, err = simpleUserClient.Options(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
|
|
|
|
// /v2/_oci/ext/discover
|
|
resp, err = simpleUserClient.Options(baseURL + "/v2/_oci/ext/discover")
|
|
So(err, ShouldBeNil)
|
|
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
|
|
})
|
|
}
|
|
|
|
func TestHtpasswdTwoCreds(t *testing.T) {
|
|
Convey("Two creds", t, func() {
|
|
twoCredTests := []string{}
|
|
user1 := "alicia"
|
|
password1 := "aliciapassword"
|
|
user2 := "bob"
|
|
password2 := "robert"
|
|
twoCredTests = append(twoCredTests, getCredString(user1, password1)+"\n"+
|
|
getCredString(user2, password2))
|
|
|
|
twoCredTests = append(twoCredTests, getCredString(user1, password1)+"\n"+
|
|
getCredString(user2, password2)+"\n")
|
|
|
|
twoCredTests = append(twoCredTests, getCredString(user1, password1)+"\n\n"+
|
|
getCredString(user2, password2)+"\n\n")
|
|
|
|
for _, testString := range twoCredTests {
|
|
func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(testString)
|
|
defer os.Remove(htpasswdPath)
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
ctlr := makeController(conf, t.TempDir())
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// with creds, should get expected status code
|
|
resp, _ := resty.R().SetBasicAuth(user1, password1).Get(baseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
resp, _ = resty.R().SetBasicAuth(user2, password2).Get(baseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// with invalid creds, it should fail
|
|
resp, _ = resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
}()
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHtpasswdFiveCreds(t *testing.T) {
|
|
Convey("Five creds", t, func() {
|
|
tests := map[string]string{
|
|
"michael": "scott",
|
|
"jim": "halpert",
|
|
"dwight": "shrute",
|
|
"pam": "bessley",
|
|
"creed": "bratton",
|
|
}
|
|
credString := strings.Builder{}
|
|
for key, val := range tests {
|
|
credString.WriteString(getCredString(key, val) + "\n")
|
|
}
|
|
|
|
func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(credString.String())
|
|
defer os.Remove(htpasswdPath)
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// with creds, should get expected status code
|
|
for key, val := range tests {
|
|
resp, _ := resty.R().SetBasicAuth(key, val).Get(baseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
}
|
|
|
|
// with invalid creds, it should fail
|
|
resp, _ := resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
}()
|
|
})
|
|
}
|
|
|
|
func TestRatelimit(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
rate := 1
|
|
conf.HTTP.Ratelimit = &config.RatelimitConfig{
|
|
Rate: &rate,
|
|
}
|
|
ctlr := makeController(conf, t.TempDir())
|
|
cm := test.NewControllerManager(ctlr)
|
|
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
Convey("Ratelimit", func() {
|
|
client := resty.New()
|
|
// first request should succeed
|
|
resp, err := client.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
// second request back-to-back should fail
|
|
resp, err = client.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusTooManyRequests)
|
|
})
|
|
})
|
|
|
|
Convey("Make a new controller", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
conf.HTTP.Ratelimit = &config.RatelimitConfig{
|
|
Methods: []config.MethodRatelimitConfig{
|
|
{
|
|
Method: http.MethodGet,
|
|
Rate: 1,
|
|
},
|
|
},
|
|
}
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
Convey("Method Ratelimit", func() {
|
|
client := resty.New()
|
|
// first request should succeed
|
|
resp, err := client.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
// second request back-to-back should fail
|
|
resp, err = client.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusTooManyRequests)
|
|
})
|
|
})
|
|
|
|
Convey("Make a new controller", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
rate := 1
|
|
conf.HTTP.Ratelimit = &config.RatelimitConfig{
|
|
Rate: &rate, // this dominates
|
|
Methods: []config.MethodRatelimitConfig{
|
|
{
|
|
Method: http.MethodGet,
|
|
Rate: 100,
|
|
},
|
|
},
|
|
}
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
Convey("Global and Method Ratelimit", func() {
|
|
client := resty.New()
|
|
// first request should succeed
|
|
resp, err := client.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
// second request back-to-back should fail
|
|
resp, err = client.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusTooManyRequests)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestBasicAuth(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
htpasswdPath := test.MakeHtpasswdFile()
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// without creds, should get access error
|
|
resp, err := resty.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
var e apiErr.Error
|
|
err = json.Unmarshal(resp.Body(), &e)
|
|
So(err, ShouldBeNil)
|
|
|
|
// with creds, should get expected status code
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
})
|
|
}
|
|
|
|
func TestBlobReferenced(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// without creds, should get access error
|
|
resp, err := resty.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
repoName := "repo"
|
|
|
|
img := test.CreateRandomImage()
|
|
|
|
err = test.UploadImage(img, baseURL, repoName, "1.0")
|
|
So(err, ShouldBeNil)
|
|
|
|
manifestDigest := img.ManifestDescriptor.Digest
|
|
configDigest := img.ConfigDescriptor.Digest
|
|
|
|
// delete manifest blob
|
|
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/blobs/" + manifestDigest.String())
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
|
|
|
|
// delete config blob
|
|
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/blobs/" + configDigest.String())
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
|
|
|
|
// delete manifest with manifest api method
|
|
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + manifestDigest.String())
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
|
|
// delete blob should work after manifest is deleted
|
|
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/blobs/" + configDigest.String())
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
})
|
|
}
|
|
|
|
func TestInterruptedBlobUpload(t *testing.T) {
|
|
Convey("Successfully cleaning interrupted blob uploads", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
client := resty.New()
|
|
blob := make([]byte, 200*1024*1024)
|
|
digest := godigest.FromBytes(blob).String()
|
|
|
|
//nolint: dupl
|
|
Convey("Test interrupt PATCH blob upload", func() {
|
|
resp, err := client.R().Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
|
|
loc := resp.Header().Get("Location")
|
|
splittedLoc := strings.Split(loc, "/")
|
|
sessionID := splittedLoc[len(splittedLoc)-1]
|
|
|
|
ctx := context.Background()
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
// patch blob
|
|
go func(ctx context.Context) {
|
|
for i := 0; i < 3; i++ {
|
|
_, _ = client.R().
|
|
SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetQueryParam("digest", digest).
|
|
SetBody(blob).
|
|
SetContext(ctx).
|
|
Patch(baseURL + loc)
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
}(ctx)
|
|
|
|
// if the blob upload has started then interrupt by running cancel()
|
|
for {
|
|
n, err := ctlr.StoreController.DefaultStore.GetBlobUpload(AuthorizedNamespace, sessionID)
|
|
if n > 0 && err == nil {
|
|
cancel()
|
|
|
|
break
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
// wait for zot to remove blobUpload
|
|
time.Sleep(1 * time.Second)
|
|
|
|
resp, err = client.R().Get(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/" + sessionID)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
})
|
|
|
|
//nolint: dupl
|
|
Convey("Test negative interrupt PATCH blob upload", func() {
|
|
resp, err := client.R().Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
|
|
loc := resp.Header().Get("Location")
|
|
splittedLoc := strings.Split(loc, "/")
|
|
sessionID := splittedLoc[len(splittedLoc)-1]
|
|
|
|
ctx := context.Background()
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
// patch blob
|
|
go func(ctx context.Context) {
|
|
for i := 0; i < 3; i++ {
|
|
_, _ = client.R().
|
|
SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetQueryParam("digest", digest).
|
|
SetBody(blob).
|
|
SetContext(ctx).
|
|
Patch(baseURL + loc)
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
}(ctx)
|
|
|
|
// if the blob upload has started then interrupt by running cancel()
|
|
for {
|
|
n, err := ctlr.StoreController.DefaultStore.GetBlobUpload(AuthorizedNamespace, sessionID)
|
|
if n > 0 && err == nil {
|
|
// cleaning blob uploads, so that zot fails to clean up, +code coverage
|
|
err = ctlr.StoreController.DefaultStore.DeleteBlobUpload(AuthorizedNamespace, sessionID)
|
|
So(err, ShouldBeNil)
|
|
cancel()
|
|
|
|
break
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
// wait for zot to remove blobUpload
|
|
time.Sleep(1 * time.Second)
|
|
|
|
resp, err = client.R().Get(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/" + sessionID)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
})
|
|
|
|
//nolint: dupl
|
|
Convey("Test interrupt PUT blob upload", func() {
|
|
resp, err := client.R().Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
|
|
loc := resp.Header().Get("Location")
|
|
splittedLoc := strings.Split(loc, "/")
|
|
sessionID := splittedLoc[len(splittedLoc)-1]
|
|
|
|
ctx := context.Background()
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
// put blob
|
|
go func(ctx context.Context) {
|
|
for i := 0; i < 3; i++ {
|
|
_, _ = client.R().
|
|
SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetQueryParam("digest", digest).
|
|
SetBody(blob).
|
|
SetContext(ctx).
|
|
Put(baseURL + loc)
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
}(ctx)
|
|
|
|
// if the blob upload has started then interrupt by running cancel()
|
|
for {
|
|
n, err := ctlr.StoreController.DefaultStore.GetBlobUpload(AuthorizedNamespace, sessionID)
|
|
if n > 0 && err == nil {
|
|
cancel()
|
|
|
|
break
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
// wait for zot to try to remove blobUpload
|
|
time.Sleep(1 * time.Second)
|
|
|
|
resp, err = client.R().Get(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/" + sessionID)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
})
|
|
|
|
//nolint: dupl
|
|
Convey("Test negative interrupt PUT blob upload", func() {
|
|
resp, err := client.R().Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
|
|
loc := resp.Header().Get("Location")
|
|
splittedLoc := strings.Split(loc, "/")
|
|
sessionID := splittedLoc[len(splittedLoc)-1]
|
|
|
|
ctx := context.Background()
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
// push blob
|
|
go func(ctx context.Context) {
|
|
for i := 0; i < 3; i++ {
|
|
_, _ = client.R().
|
|
SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetQueryParam("digest", digest).
|
|
SetBody(blob).
|
|
SetContext(ctx).
|
|
Put(baseURL + loc)
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
}(ctx)
|
|
|
|
// if the blob upload has started then interrupt by running cancel()
|
|
for {
|
|
n, err := ctlr.StoreController.DefaultStore.GetBlobUpload(AuthorizedNamespace, sessionID)
|
|
if n > 0 && err == nil {
|
|
// cleaning blob uploads, so that zot fails to clean up, +code coverage
|
|
err = ctlr.StoreController.DefaultStore.DeleteBlobUpload(AuthorizedNamespace, sessionID)
|
|
So(err, ShouldBeNil)
|
|
cancel()
|
|
|
|
break
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
// wait for zot to try to remove blobUpload
|
|
time.Sleep(1 * time.Second)
|
|
|
|
resp, err = client.R().Get(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/" + sessionID)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestMultipleInstance(t *testing.T) {
|
|
Convey("Negative test zot multiple instance", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
htpasswdPath := test.MakeHtpasswdFile()
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
ctlr := api.NewController(conf)
|
|
err := ctlr.Init(context.Background())
|
|
So(err, ShouldEqual, errors.ErrImgStoreNotFound)
|
|
|
|
globalDir := t.TempDir()
|
|
subDir := t.TempDir()
|
|
ctlr.Config.Storage.RootDirectory = globalDir
|
|
|
|
subPathMap := make(map[string]config.StorageConfig)
|
|
|
|
subPathMap["/a"] = config.StorageConfig{RootDirectory: subDir}
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
client := resty.New()
|
|
|
|
tagResponse, err := client.R().SetBasicAuth(username, passphrase).
|
|
Get(baseURL + "/v2/zot-test/tags/list")
|
|
So(err, ShouldBeNil)
|
|
So(tagResponse.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
})
|
|
|
|
Convey("Test zot multiple instance", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
htpasswdPath := test.MakeHtpasswdFile()
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
globalDir := t.TempDir()
|
|
subDir := t.TempDir()
|
|
ctlr := makeController(conf, globalDir)
|
|
subPathMap := make(map[string]config.StorageConfig)
|
|
subPathMap["/a"] = config.StorageConfig{RootDirectory: subDir}
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// without creds, should get access error
|
|
resp, err := resty.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
var e apiErr.Error
|
|
err = json.Unmarshal(resp.Body(), &e)
|
|
So(err, ShouldBeNil)
|
|
|
|
// with creds, should get expected status code
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
})
|
|
|
|
Convey("Test zot multiple subpath with same root directory", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
htpasswdPath := test.MakeHtpasswdFile()
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
globalDir := t.TempDir()
|
|
subDir := t.TempDir()
|
|
|
|
ctlr := makeController(conf, globalDir)
|
|
subPathMap := make(map[string]config.StorageConfig)
|
|
subPathMap["/a"] = config.StorageConfig{RootDirectory: globalDir, Dedupe: true, GC: true}
|
|
subPathMap["/b"] = config.StorageConfig{RootDirectory: subDir, Dedupe: true, GC: true}
|
|
|
|
ctlr.Config.Storage.SubPaths = subPathMap
|
|
|
|
err := ctlr.Init(context.Background())
|
|
So(err, ShouldNotBeNil)
|
|
|
|
// subpath root directory does not exist.
|
|
subPathMap["/a"] = config.StorageConfig{RootDirectory: globalDir, Dedupe: true, GC: true}
|
|
subPathMap["/b"] = config.StorageConfig{RootDirectory: subDir, Dedupe: false, GC: true}
|
|
|
|
ctlr.Config.Storage.SubPaths = subPathMap
|
|
|
|
err = ctlr.Init(context.Background())
|
|
So(err, ShouldNotBeNil)
|
|
|
|
subPathMap["/a"] = config.StorageConfig{RootDirectory: subDir, Dedupe: true, GC: true}
|
|
subPathMap["/b"] = config.StorageConfig{RootDirectory: subDir, Dedupe: true, GC: true}
|
|
|
|
ctlr.Config.Storage.SubPaths = subPathMap
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// without creds, should get access error
|
|
resp, err := resty.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
var e apiErr.Error
|
|
err = json.Unmarshal(resp.Body(), &e)
|
|
So(err, ShouldBeNil)
|
|
|
|
// with creds, should get expected status code
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
})
|
|
}
|
|
|
|
func TestTLSWithBasicAuth(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
caCert, err := os.ReadFile(CACert)
|
|
So(err, ShouldBeNil)
|
|
caCertPool := x509.NewCertPool()
|
|
caCertPool.AppendCertsFromPEM(caCert)
|
|
htpasswdPath := test.MakeHtpasswdFile()
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
secureBaseURL := test.GetSecureBaseURL(port)
|
|
|
|
resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
|
|
defer func() { resty.SetTLSClientConfig(nil) }()
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
conf.HTTP.TLS = &config.TLSConfig{
|
|
Cert: ServerCert,
|
|
Key: ServerKey,
|
|
}
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// accessing insecure HTTP site should fail
|
|
resp, err := resty.R().Get(baseURL)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
|
|
// without creds, should get access error
|
|
resp, err = resty.R().Get(secureBaseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
var e apiErr.Error
|
|
err = json.Unmarshal(resp.Body(), &e)
|
|
So(err, ShouldBeNil)
|
|
|
|
// with creds, should get expected status code
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
})
|
|
}
|
|
|
|
func TestTLSWithBasicAuthAllowReadAccess(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
caCert, err := os.ReadFile(CACert)
|
|
So(err, ShouldBeNil)
|
|
caCertPool := x509.NewCertPool()
|
|
caCertPool.AppendCertsFromPEM(caCert)
|
|
htpasswdPath := test.MakeHtpasswdFile()
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
secureBaseURL := test.GetSecureBaseURL(port)
|
|
|
|
resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
|
|
defer func() { resty.SetTLSClientConfig(nil) }()
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
conf.HTTP.TLS = &config.TLSConfig{
|
|
Cert: ServerCert,
|
|
Key: ServerKey,
|
|
}
|
|
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Repositories: config.Repositories{
|
|
AuthorizationAllRepos: config.PolicyGroup{
|
|
AnonymousPolicy: []string{"read"},
|
|
},
|
|
},
|
|
}
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// accessing insecure HTTP site should fail
|
|
resp, err := resty.R().Get(baseURL)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
|
|
// without creds, should still be allowed to access
|
|
resp, err = resty.R().Get(secureBaseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// with creds, should get expected status code
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// without creds, writes should fail
|
|
resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
})
|
|
}
|
|
|
|
func TestMutualTLSAuthWithUserPermissions(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
caCert, err := os.ReadFile(CACert)
|
|
So(err, ShouldBeNil)
|
|
caCertPool := x509.NewCertPool()
|
|
caCertPool.AppendCertsFromPEM(caCert)
|
|
htpasswdPath := test.MakeHtpasswdFile()
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
secureBaseURL := test.GetSecureBaseURL(port)
|
|
|
|
resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
|
|
defer func() { resty.SetTLSClientConfig(nil) }()
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
conf.HTTP.TLS = &config.TLSConfig{
|
|
Cert: ServerCert,
|
|
Key: ServerKey,
|
|
CACert: CACert,
|
|
}
|
|
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Repositories: config.Repositories{
|
|
AuthorizationAllRepos: config.PolicyGroup{
|
|
Policies: []config.Policy{
|
|
{
|
|
Users: []string{"*"},
|
|
Actions: []string{"read"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
resp, err := resty.R().Get(baseURL)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
|
|
repoPolicy := conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos]
|
|
|
|
// setup TLS mutual auth
|
|
cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key")
|
|
So(err, ShouldBeNil)
|
|
|
|
resty.SetCertificates(cert)
|
|
defer func() { resty.SetCertificates(tls.Certificate{}) }()
|
|
|
|
// with client certs but without creds, should succeed
|
|
resp, err = resty.R().Get(secureBaseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
resp, err = resty.R().Get(secureBaseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// with creds, should get expected status code
|
|
resp, _ = resty.R().Get(secureBaseURL)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
// reading a repo should not get 403
|
|
resp, err = resty.R().Get(secureBaseURL + "/v2/repo/tags/list")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
// without creds, writes should fail
|
|
resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
// empty default authorization and give user the permission to create
|
|
repoPolicy.Policies[0].Actions = append(repoPolicy.Policies[0].Actions, "create")
|
|
conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy
|
|
resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
})
|
|
}
|
|
|
|
func TestMutualTLSAuthWithoutCN(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
caCert, err := os.ReadFile("../../test/data/noidentity/ca.crt")
|
|
So(err, ShouldBeNil)
|
|
caCertPool := x509.NewCertPool()
|
|
caCertPool.AppendCertsFromPEM(caCert)
|
|
|
|
port := test.GetFreePort()
|
|
secureBaseURL := test.GetSecureBaseURL(port)
|
|
|
|
resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
|
|
defer func() { resty.SetTLSClientConfig(nil) }()
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
conf.HTTP.TLS = &config.TLSConfig{
|
|
Cert: "../../test/data/noidentity/server.cert",
|
|
Key: "../../test/data/noidentity/server.key",
|
|
CACert: "../../test/data/noidentity/ca.crt",
|
|
}
|
|
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Repositories: config.Repositories{
|
|
AuthorizationAllRepos: config.PolicyGroup{
|
|
Policies: []config.Policy{
|
|
{
|
|
Users: []string{"*"},
|
|
Actions: []string{"read"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// setup TLS mutual auth
|
|
cert, err := tls.LoadX509KeyPair("../../test/data/noidentity/client.cert", "../../test/data/noidentity/client.key")
|
|
So(err, ShouldBeNil)
|
|
|
|
resty.SetCertificates(cert)
|
|
defer func() { resty.SetCertificates(tls.Certificate{}) }()
|
|
|
|
// with client certs but without TLS mutual auth setup should get certificate error
|
|
resp, _ := resty.R().Get(secureBaseURL + "/v2/_catalog")
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
})
|
|
}
|
|
|
|
func TestTLSMutualAuth(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
caCert, err := os.ReadFile(CACert)
|
|
So(err, ShouldBeNil)
|
|
caCertPool := x509.NewCertPool()
|
|
caCertPool.AppendCertsFromPEM(caCert)
|
|
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
secureBaseURL := test.GetSecureBaseURL(port)
|
|
|
|
resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
|
|
defer func() { resty.SetTLSClientConfig(nil) }()
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
conf.HTTP.TLS = &config.TLSConfig{
|
|
Cert: ServerCert,
|
|
Key: ServerKey,
|
|
CACert: CACert,
|
|
}
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// accessing insecure HTTP site should fail
|
|
resp, err := resty.R().Get(baseURL)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
|
|
// without client certs and creds, should get conn error
|
|
_, err = resty.R().Get(secureBaseURL)
|
|
So(err, ShouldNotBeNil)
|
|
|
|
// with creds but without certs, should get conn error
|
|
_, err = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
|
|
So(err, ShouldNotBeNil)
|
|
|
|
// setup TLS mutual auth
|
|
cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key")
|
|
So(err, ShouldBeNil)
|
|
|
|
resty.SetCertificates(cert)
|
|
defer func() { resty.SetCertificates(tls.Certificate{}) }()
|
|
|
|
// with client certs but without creds, should succeed
|
|
resp, err = resty.R().Get(secureBaseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// with client certs and creds, should get expected status code
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
// with client certs, creds shouldn't matter
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
})
|
|
}
|
|
|
|
func TestTLSMutualAuthAllowReadAccess(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
caCert, err := os.ReadFile(CACert)
|
|
So(err, ShouldBeNil)
|
|
caCertPool := x509.NewCertPool()
|
|
caCertPool.AppendCertsFromPEM(caCert)
|
|
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
secureBaseURL := test.GetSecureBaseURL(port)
|
|
|
|
resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
|
|
defer func() { resty.SetTLSClientConfig(nil) }()
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
conf.HTTP.TLS = &config.TLSConfig{
|
|
Cert: ServerCert,
|
|
Key: ServerKey,
|
|
CACert: CACert,
|
|
}
|
|
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Repositories: config.Repositories{
|
|
AuthorizationAllRepos: config.PolicyGroup{
|
|
AnonymousPolicy: []string{"read"},
|
|
},
|
|
},
|
|
}
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// accessing insecure HTTP site should fail
|
|
resp, err := resty.R().Get(baseURL)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
|
|
// without client certs and creds, reads are allowed
|
|
resp, err = resty.R().Get(secureBaseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// with creds but without certs, reads are allowed
|
|
resp, err = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// without creds, writes should fail
|
|
resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
// setup TLS mutual auth
|
|
cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key")
|
|
So(err, ShouldBeNil)
|
|
|
|
resty.SetCertificates(cert)
|
|
defer func() { resty.SetCertificates(tls.Certificate{}) }()
|
|
|
|
// with client certs but without creds, should succeed
|
|
resp, err = resty.R().Get(secureBaseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// with client certs and creds, should get expected status code
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
// with client certs, creds shouldn't matter
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
})
|
|
}
|
|
|
|
func TestTLSMutualAndBasicAuth(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
caCert, err := os.ReadFile(CACert)
|
|
So(err, ShouldBeNil)
|
|
caCertPool := x509.NewCertPool()
|
|
caCertPool.AppendCertsFromPEM(caCert)
|
|
htpasswdPath := test.MakeHtpasswdFile()
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
secureBaseURL := test.GetSecureBaseURL(port)
|
|
|
|
resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
|
|
defer func() { resty.SetTLSClientConfig(nil) }()
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
conf.HTTP.TLS = &config.TLSConfig{
|
|
Cert: ServerCert,
|
|
Key: ServerKey,
|
|
CACert: CACert,
|
|
}
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// accessing insecure HTTP site should fail
|
|
resp, err := resty.R().Get(baseURL)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
|
|
// without client certs and creds, should fail
|
|
_, err = resty.R().Get(secureBaseURL)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
|
|
// with creds but without certs, should succeed
|
|
_, err = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
|
|
// setup TLS mutual auth
|
|
cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key")
|
|
So(err, ShouldBeNil)
|
|
|
|
resty.SetCertificates(cert)
|
|
defer func() { resty.SetCertificates(tls.Certificate{}) }()
|
|
|
|
// with client certs but without creds, should get access error
|
|
resp, err = resty.R().Get(secureBaseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
// with client certs and creds, should get expected status code
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
})
|
|
}
|
|
|
|
func TestTLSMutualAndBasicAuthAllowReadAccess(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
caCert, err := os.ReadFile(CACert)
|
|
So(err, ShouldBeNil)
|
|
caCertPool := x509.NewCertPool()
|
|
caCertPool.AppendCertsFromPEM(caCert)
|
|
htpasswdPath := test.MakeHtpasswdFile()
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
secureBaseURL := test.GetSecureBaseURL(port)
|
|
|
|
resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
|
|
defer func() { resty.SetTLSClientConfig(nil) }()
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
conf.HTTP.TLS = &config.TLSConfig{
|
|
Cert: ServerCert,
|
|
Key: ServerKey,
|
|
CACert: CACert,
|
|
}
|
|
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Repositories: config.Repositories{
|
|
AuthorizationAllRepos: config.PolicyGroup{
|
|
AnonymousPolicy: []string{"read"},
|
|
},
|
|
},
|
|
}
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// accessing insecure HTTP site should fail
|
|
resp, err := resty.R().Get(baseURL)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
|
|
// without client certs and creds, should fail
|
|
_, err = resty.R().Get(secureBaseURL)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
|
|
// with creds but without certs, should succeed
|
|
_, err = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
|
|
// setup TLS mutual auth
|
|
cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key")
|
|
So(err, ShouldBeNil)
|
|
|
|
resty.SetCertificates(cert)
|
|
defer func() { resty.SetCertificates(tls.Certificate{}) }()
|
|
|
|
// with client certs but without creds, reads should succeed
|
|
resp, err = resty.R().Get(secureBaseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// with only client certs, writes should fail
|
|
resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
// with client certs and creds, should get expected status code
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
})
|
|
}
|
|
|
|
const (
|
|
LDAPAddress = "127.0.0.1"
|
|
LDAPBaseDN = "ou=test"
|
|
LDAPBindDN = "cn=reader," + LDAPBaseDN
|
|
LDAPBindPassword = "bindPassword"
|
|
)
|
|
|
|
type testLDAPServer struct {
|
|
server *vldap.Server
|
|
quitCh chan bool
|
|
}
|
|
|
|
func newTestLDAPServer() *testLDAPServer {
|
|
ldaps := &testLDAPServer{}
|
|
quitCh := make(chan bool)
|
|
server := vldap.NewServer()
|
|
server.QuitChannel(quitCh)
|
|
server.BindFunc("", ldaps)
|
|
server.SearchFunc("", ldaps)
|
|
ldaps.server = server
|
|
ldaps.quitCh = quitCh
|
|
|
|
return ldaps
|
|
}
|
|
|
|
func (l *testLDAPServer) Start(port int) {
|
|
addr := fmt.Sprintf("%s:%d", LDAPAddress, port)
|
|
|
|
go func() {
|
|
if err := l.server.ListenAndServe(addr); err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
|
|
for {
|
|
_, err := net.Dial("tcp", addr)
|
|
if err == nil {
|
|
break
|
|
}
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func (l *testLDAPServer) Stop() {
|
|
l.quitCh <- true
|
|
}
|
|
|
|
func (l *testLDAPServer) Bind(bindDN, bindSimplePw string, conn net.Conn) (vldap.LDAPResultCode, error) {
|
|
if bindDN == "" || bindSimplePw == "" {
|
|
return vldap.LDAPResultInappropriateAuthentication, errors.ErrRequireCred
|
|
}
|
|
|
|
if (bindDN == LDAPBindDN && bindSimplePw == LDAPBindPassword) ||
|
|
(bindDN == fmt.Sprintf("cn=%s,%s", username, LDAPBaseDN) && bindSimplePw == passphrase) {
|
|
return vldap.LDAPResultSuccess, nil
|
|
}
|
|
|
|
return vldap.LDAPResultInvalidCredentials, errors.ErrInvalidCred
|
|
}
|
|
|
|
func (l *testLDAPServer) Search(boundDN string, req vldap.SearchRequest,
|
|
conn net.Conn,
|
|
) (vldap.ServerSearchResult, error) {
|
|
check := fmt.Sprintf("(uid=%s)", username)
|
|
if check == req.Filter {
|
|
return vldap.ServerSearchResult{
|
|
Entries: []*vldap.Entry{
|
|
{
|
|
DN: fmt.Sprintf("cn=%s,%s", username, LDAPBaseDN),
|
|
Attributes: []*vldap.EntryAttribute{
|
|
{
|
|
Name: "memberOf",
|
|
Values: []string{group},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ResultCode: vldap.LDAPResultSuccess,
|
|
}, nil
|
|
}
|
|
|
|
return vldap.ServerSearchResult{}, nil
|
|
}
|
|
|
|
func TestBasicAuthWithLDAP(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
l := newTestLDAPServer()
|
|
port := test.GetFreePort()
|
|
ldapPort, err := strconv.Atoi(port)
|
|
So(err, ShouldBeNil)
|
|
l.Start(ldapPort)
|
|
defer l.Stop()
|
|
|
|
port = test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
LDAP: &config.LDAPConfig{
|
|
Insecure: true,
|
|
Address: LDAPAddress,
|
|
Port: ldapPort,
|
|
BindDN: LDAPBindDN,
|
|
BindPassword: LDAPBindPassword,
|
|
BaseDN: LDAPBaseDN,
|
|
UserAttribute: "uid",
|
|
},
|
|
}
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// without creds, should get access error
|
|
resp, err := resty.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
var e apiErr.Error
|
|
err = json.Unmarshal(resp.Body(), &e)
|
|
So(err, ShouldBeNil)
|
|
|
|
// with creds, should get expected status code
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// missing password
|
|
resp, _ = resty.R().SetBasicAuth(username, "").Get(baseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
})
|
|
}
|
|
|
|
func TestGroupsPermissionsForLDAP(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
l := newTestLDAPServer()
|
|
port := test.GetFreePort()
|
|
ldapPort, err := strconv.Atoi(port)
|
|
So(err, ShouldBeNil)
|
|
l.Start(ldapPort)
|
|
defer l.Stop()
|
|
|
|
port = test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
tempDir := t.TempDir()
|
|
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
LDAP: &config.LDAPConfig{
|
|
Insecure: true,
|
|
Address: LDAPAddress,
|
|
Port: ldapPort,
|
|
BindDN: LDAPBindDN,
|
|
BindPassword: LDAPBindPassword,
|
|
BaseDN: LDAPBaseDN,
|
|
UserAttribute: "uid",
|
|
UserGroupAttribute: "memberOf",
|
|
},
|
|
}
|
|
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Groups: config.Groups{
|
|
group: {
|
|
Users: []string{username},
|
|
},
|
|
},
|
|
Repositories: config.Repositories{
|
|
repo: config.PolicyGroup{
|
|
Policies: []config.Policy{
|
|
{
|
|
Groups: []string{group},
|
|
Actions: []string{"read", "create"},
|
|
},
|
|
},
|
|
DefaultPolicy: []string{},
|
|
},
|
|
},
|
|
AdminPolicy: config.Policy{
|
|
Users: []string{},
|
|
Actions: []string{},
|
|
},
|
|
}
|
|
|
|
ctlr := makeController(conf, tempDir)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
img := test.CreateDefaultImage()
|
|
|
|
err = test.UploadImageWithBasicAuth(
|
|
img, baseURL, repo, img.DigestStr(),
|
|
username, passphrase)
|
|
So(err, ShouldBeNil)
|
|
})
|
|
}
|
|
|
|
func TestLDAPFailures(t *testing.T) {
|
|
Convey("Make a LDAP conn", t, func() {
|
|
l := newTestLDAPServer()
|
|
port := test.GetFreePort()
|
|
ldapPort, err := strconv.Atoi(port)
|
|
So(err, ShouldBeNil)
|
|
l.Start(ldapPort)
|
|
defer l.Stop()
|
|
|
|
Convey("Empty config", func() {
|
|
lc := &api.LDAPClient{}
|
|
err := lc.Connect()
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Basic connectivity config", func() {
|
|
lc := &api.LDAPClient{
|
|
Host: LDAPAddress,
|
|
Port: ldapPort,
|
|
}
|
|
err := lc.Connect()
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Basic TLS connectivity config", func() {
|
|
lc := &api.LDAPClient{
|
|
Host: LDAPAddress,
|
|
Port: ldapPort,
|
|
UseSSL: true,
|
|
}
|
|
err := lc.Connect()
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestBearerAuth(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
authTestServer := test.MakeAuthTestServer(ServerKey, UnauthorizedNamespace)
|
|
defer authTestServer.Close()
|
|
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
aurl, err := url.Parse(authTestServer.URL)
|
|
So(err, ShouldBeNil)
|
|
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
Bearer: &config.BearerConfig{
|
|
Cert: ServerCert,
|
|
Realm: authTestServer.URL + "/auth/token",
|
|
Service: aurl.Host,
|
|
},
|
|
}
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
blob := []byte("hello, blob!")
|
|
digest := godigest.FromBytes(blob).String()
|
|
|
|
resp, err := resty.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
authorizationHeader := test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
|
|
resp, err = resty.R().
|
|
SetQueryParam("service", authorizationHeader.Service).
|
|
SetQueryParam("scope", authorizationHeader.Scope).
|
|
Get(authorizationHeader.Realm)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
var goodToken test.AccessTokenResponse
|
|
err = json.Unmarshal(resp.Body(), &goodToken)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().
|
|
SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
|
|
Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
resp, err = resty.R().SetHeader("Authorization",
|
|
fmt.Sprintf("Bearer %s", goodToken.AccessToken)).Options(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
|
|
|
|
resp, err = resty.R().Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
authorizationHeader = test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
|
|
resp, err = resty.R().
|
|
SetQueryParam("service", authorizationHeader.Service).
|
|
SetQueryParam("scope", authorizationHeader.Scope).
|
|
Get(authorizationHeader.Realm)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
err = json.Unmarshal(resp.Body(), &goodToken)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().
|
|
SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
|
|
Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc := resp.Header().Get("Location")
|
|
|
|
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)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
authorizationHeader = test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
|
|
resp, err = resty.R().
|
|
SetQueryParam("service", authorizationHeader.Service).
|
|
SetQueryParam("scope", authorizationHeader.Scope).
|
|
Get(authorizationHeader.Realm)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
err = json.Unmarshal(resp.Body(), &goodToken)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().
|
|
SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
|
|
SetQueryParam("digest", digest).
|
|
SetBody(blob).
|
|
Put(baseURL + loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
resp, err = resty.R().
|
|
SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
|
|
Get(baseURL + "/v2/" + AuthorizedNamespace + "/tags/list")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
authorizationHeader = test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
|
|
resp, err = resty.R().
|
|
SetQueryParam("service", authorizationHeader.Service).
|
|
SetQueryParam("scope", authorizationHeader.Scope).
|
|
Get(authorizationHeader.Realm)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
err = json.Unmarshal(resp.Body(), &goodToken)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().
|
|
SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
|
|
Get(baseURL + "/v2/" + AuthorizedNamespace + "/tags/list")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
resp, err = resty.R().
|
|
Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
authorizationHeader = test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
|
|
resp, err = resty.R().
|
|
SetQueryParam("service", authorizationHeader.Service).
|
|
SetQueryParam("scope", authorizationHeader.Scope).
|
|
Get(authorizationHeader.Realm)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
var badToken test.AccessTokenResponse
|
|
err = json.Unmarshal(resp.Body(), &badToken)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().
|
|
SetHeader("Authorization", fmt.Sprintf("Bearer %s", badToken.AccessToken)).
|
|
Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
})
|
|
}
|
|
|
|
func TestBearerAuthWrongAuthorizer(t *testing.T) {
|
|
Convey("Make a new authorizer", t, func() {
|
|
port := test.GetFreePort()
|
|
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
Bearer: &config.BearerConfig{
|
|
Cert: "bla",
|
|
Realm: "blabla",
|
|
Service: "blablabla",
|
|
},
|
|
}
|
|
ctlr := makeController(conf, t.TempDir())
|
|
cm := test.NewControllerManager(ctlr)
|
|
|
|
So(func() {
|
|
ctx := context.Background()
|
|
cm.RunServer(ctx)
|
|
}, ShouldPanic)
|
|
})
|
|
}
|
|
|
|
func TestBearerAuthWithAllowReadAccess(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
authTestServer := test.MakeAuthTestServer(ServerKey, UnauthorizedNamespace)
|
|
defer authTestServer.Close()
|
|
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
aurl, err := url.Parse(authTestServer.URL)
|
|
So(err, ShouldBeNil)
|
|
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
Bearer: &config.BearerConfig{
|
|
Cert: ServerCert,
|
|
Realm: authTestServer.URL + "/auth/token",
|
|
Service: aurl.Host,
|
|
},
|
|
}
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Repositories: config.Repositories{
|
|
AuthorizationAllRepos: config.PolicyGroup{
|
|
AnonymousPolicy: []string{"read"},
|
|
},
|
|
},
|
|
}
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
blob := []byte("hello, blob!")
|
|
digest := godigest.FromBytes(blob).String()
|
|
|
|
resp, err := resty.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
authorizationHeader := test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
|
|
resp, err = resty.R().
|
|
SetQueryParam("service", authorizationHeader.Service).
|
|
SetQueryParam("scope", authorizationHeader.Scope).
|
|
Get(authorizationHeader.Realm)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
var goodToken test.AccessTokenResponse
|
|
err = json.Unmarshal(resp.Body(), &goodToken)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().
|
|
SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
|
|
Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
resp, err = resty.R().Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
authorizationHeader = test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
|
|
resp, err = resty.R().
|
|
SetQueryParam("service", authorizationHeader.Service).
|
|
SetQueryParam("scope", authorizationHeader.Scope).
|
|
Get(authorizationHeader.Realm)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
err = json.Unmarshal(resp.Body(), &goodToken)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().
|
|
SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
|
|
Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc := resp.Header().Get("Location")
|
|
|
|
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)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
authorizationHeader = test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
|
|
resp, err = resty.R().
|
|
SetQueryParam("service", authorizationHeader.Service).
|
|
SetQueryParam("scope", authorizationHeader.Scope).
|
|
Get(authorizationHeader.Realm)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
err = json.Unmarshal(resp.Body(), &goodToken)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().
|
|
SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
|
|
SetQueryParam("digest", digest).
|
|
SetBody(blob).
|
|
Put(baseURL + loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
resp, err = resty.R().
|
|
SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
|
|
Get(baseURL + "/v2/" + AuthorizedNamespace + "/tags/list")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
authorizationHeader = test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
|
|
resp, err = resty.R().
|
|
SetQueryParam("service", authorizationHeader.Service).
|
|
SetQueryParam("scope", authorizationHeader.Scope).
|
|
Get(authorizationHeader.Realm)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
err = json.Unmarshal(resp.Body(), &goodToken)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().
|
|
SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
|
|
Get(baseURL + "/v2/" + AuthorizedNamespace + "/tags/list")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
resp, err = resty.R().
|
|
Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
authorizationHeader = test.ParseBearerAuthHeader(resp.Header().Get("Www-Authenticate"))
|
|
resp, err = resty.R().
|
|
SetQueryParam("service", authorizationHeader.Service).
|
|
SetQueryParam("scope", authorizationHeader.Scope).
|
|
Get(authorizationHeader.Realm)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
var badToken test.AccessTokenResponse
|
|
err = json.Unmarshal(resp.Body(), &badToken)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().
|
|
SetHeader("Authorization", fmt.Sprintf("Bearer %s", badToken.AccessToken)).
|
|
Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
})
|
|
}
|
|
|
|
func TestNewRelyingPartyOIDC(t *testing.T) {
|
|
Convey("Test NewRelyingPartyOIDC", t, func() {
|
|
conf := config.New()
|
|
|
|
mockOIDCServer, err := test.MockOIDCRun()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
defer func() {
|
|
err := mockOIDCServer.Shutdown()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
|
|
mockOIDCConfig := mockOIDCServer.Config()
|
|
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
OpenID: &config.OpenIDConfig{
|
|
Providers: map[string]config.OpenIDProviderConfig{
|
|
"oidc": {
|
|
ClientID: mockOIDCConfig.ClientID,
|
|
ClientSecret: mockOIDCConfig.ClientSecret,
|
|
KeyPath: "",
|
|
Issuer: mockOIDCConfig.Issuer,
|
|
Scopes: []string{"openid", "email"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
Convey("provider not found in config", func() {
|
|
So(func() { _ = api.NewRelyingPartyOIDC(conf, "notDex") }, ShouldPanic)
|
|
})
|
|
|
|
Convey("key path not found on disk", func() {
|
|
oidcProviderCfg := conf.HTTP.Auth.OpenID.Providers["oidc"]
|
|
oidcProviderCfg.KeyPath = "path/to/file"
|
|
conf.HTTP.Auth.OpenID.Providers["oidc"] = oidcProviderCfg
|
|
|
|
So(func() { _ = api.NewRelyingPartyOIDC(conf, "oidc") }, ShouldPanic)
|
|
})
|
|
|
|
Convey("https callback", func() {
|
|
conf.HTTP.TLS = &config.TLSConfig{
|
|
Cert: ServerCert,
|
|
Key: ServerKey,
|
|
}
|
|
|
|
rp := api.NewRelyingPartyOIDC(conf, "oidc")
|
|
So(rp, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("no client secret in config", func() {
|
|
oidcProvider := conf.HTTP.Auth.OpenID.Providers["oidc"]
|
|
oidcProvider.ClientSecret = ""
|
|
conf.HTTP.Auth.OpenID.Providers["oidc"] = oidcProvider
|
|
|
|
rp := api.NewRelyingPartyOIDC(conf, "oidc")
|
|
So(rp, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("provider issuer unreachable", func() {
|
|
oidcProvider := conf.HTTP.Auth.OpenID.Providers["oidc"]
|
|
oidcProvider.Issuer = ""
|
|
conf.HTTP.Auth.OpenID.Providers["oidc"] = oidcProvider
|
|
|
|
So(func() { _ = api.NewRelyingPartyOIDC(conf, "oidc") }, ShouldPanic)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestOpenIDMiddleware(t *testing.T) {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
defaultVal := true
|
|
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
testCases := []struct {
|
|
testCaseName string
|
|
address string
|
|
externalURL string
|
|
}{
|
|
{
|
|
address: "0.0.0.0",
|
|
externalURL: fmt.Sprintf("http://%s", net.JoinHostPort(conf.HTTP.Address, conf.HTTP.Port)),
|
|
testCaseName: "with ExternalURL provided in config",
|
|
},
|
|
{
|
|
address: "127.0.0.1",
|
|
externalURL: "",
|
|
testCaseName: "without ExternalURL provided in config",
|
|
},
|
|
}
|
|
|
|
// need a username different than ldap one, to test both logic
|
|
content := fmt.Sprintf("%s:$2y$05$hlbSXDp6hzDLu6VwACS39ORvVRpr3OMR4RlJ31jtlaOEGnPjKZI1m\n", htpasswdUsername)
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(content)
|
|
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
ldapServer := newTestLDAPServer()
|
|
port = test.GetFreePort()
|
|
|
|
ldapPort, err := strconv.Atoi(port)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ldapServer.Start(ldapPort)
|
|
defer ldapServer.Stop()
|
|
|
|
mockOIDCServer, err := test.MockOIDCRun()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
defer func() {
|
|
err := mockOIDCServer.Shutdown()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
|
|
mockOIDCConfig := mockOIDCServer.Config()
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
LDAP: &config.LDAPConfig{
|
|
Insecure: true,
|
|
Address: LDAPAddress,
|
|
Port: ldapPort,
|
|
BindDN: LDAPBindDN,
|
|
BindPassword: LDAPBindPassword,
|
|
BaseDN: LDAPBaseDN,
|
|
UserAttribute: "uid",
|
|
},
|
|
OpenID: &config.OpenIDConfig{
|
|
Providers: map[string]config.OpenIDProviderConfig{
|
|
"oidc": {
|
|
ClientID: mockOIDCConfig.ClientID,
|
|
ClientSecret: mockOIDCConfig.ClientSecret,
|
|
KeyPath: "",
|
|
Issuer: mockOIDCConfig.Issuer,
|
|
Scopes: []string{"openid", "email"},
|
|
},
|
|
// just for the constructor coverage
|
|
"github": {
|
|
ClientID: mockOIDCConfig.ClientID,
|
|
ClientSecret: mockOIDCConfig.ClientSecret,
|
|
KeyPath: "",
|
|
Issuer: mockOIDCConfig.Issuer,
|
|
Scopes: []string{"openid", "email"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
searchConfig := &extconf.SearchConfig{
|
|
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
|
}
|
|
|
|
// UI is enabled because we also want to test access on the mgmt route
|
|
uiConfig := &extconf.UIConfig{
|
|
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
|
}
|
|
|
|
conf.Extensions = &extconf.ExtensionConfig{
|
|
Search: searchConfig,
|
|
UI: uiConfig,
|
|
}
|
|
|
|
ctlr := api.NewController(conf)
|
|
|
|
for _, testcase := range testCases {
|
|
t.Run(testcase.testCaseName, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
ctlr.Config.Storage.RootDirectory = dir
|
|
ctlr.Config.HTTP.ExternalURL = testcase.externalURL
|
|
ctlr.Config.HTTP.Address = testcase.address
|
|
cm := test.NewControllerManager(ctlr)
|
|
|
|
cm.StartServer()
|
|
defer cm.StopServer()
|
|
test.WaitTillServerReady(baseURL)
|
|
|
|
Convey("browser client requests", t, func() {
|
|
Convey("login with no provider supplied", func() {
|
|
client := resty.New()
|
|
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
|
|
// first login user
|
|
resp, err := client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
SetQueryParam("provider", "unknown").
|
|
Get(baseURL + constants.LoginPath)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
})
|
|
|
|
Convey("login with openid and get catalog with session", func() {
|
|
client := resty.New()
|
|
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
|
|
|
|
Convey("with callback_ui value provided", func() {
|
|
// first login user
|
|
resp, err := client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
SetQueryParam("provider", "oidc").
|
|
SetQueryParam("callback_ui", baseURL+"/v2/").
|
|
Get(baseURL + constants.LoginPath)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
})
|
|
|
|
// first login user
|
|
resp, err := client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
SetQueryParam("provider", "oidc").
|
|
Get(baseURL + constants.LoginPath)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
client.SetCookies(resp.Cookies())
|
|
|
|
// call endpoint with session (added to client after previous request)
|
|
resp, err = client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// logout with options method for coverage
|
|
resp, err = client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Options(baseURL + constants.LogoutPath)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
|
|
// logout user
|
|
resp, err = client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Post(baseURL + constants.LogoutPath)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// calling endpoint should fail with unauthorized access
|
|
resp, err = client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
})
|
|
|
|
//nolint: dupl
|
|
Convey("login with basic auth(htpasswd) and get catalog with session", func() {
|
|
client := resty.New()
|
|
|
|
// without creds, should get access error
|
|
resp, err := client.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
var e apiErr.Error
|
|
err = json.Unmarshal(resp.Body(), &e)
|
|
So(err, ShouldBeNil)
|
|
|
|
// first login user
|
|
// with creds, should get expected status code
|
|
resp, err = client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
resp, err = client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
resp, err = client.R().
|
|
SetBasicAuth(htpasswdUsername, passphrase).
|
|
Get(baseURL + constants.FullMgmt)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
client.SetCookies(resp.Cookies())
|
|
|
|
// call endpoint with session, without credentials, (added to client after previous request)
|
|
resp, err = client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
resp, err = client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Get(baseURL + constants.FullMgmt)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// logout user
|
|
resp, err = client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Post(baseURL + constants.LogoutPath)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// calling endpoint should fail with unauthorized access
|
|
resp, err = client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
})
|
|
|
|
//nolint: dupl
|
|
Convey("login with ldap and get catalog", func() {
|
|
client := resty.New()
|
|
|
|
// without creds, should get access error
|
|
resp, err := client.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
var e apiErr.Error
|
|
err = json.Unmarshal(resp.Body(), &e)
|
|
So(err, ShouldBeNil)
|
|
|
|
// first login user
|
|
// with creds, should get expected status code
|
|
resp, err = client.R().SetBasicAuth(username, passphrase).Get(baseURL)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
resp, err = client.R().SetBasicAuth(username, passphrase).Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
resp, err = client.R().
|
|
SetBasicAuth(username, passphrase).
|
|
Get(baseURL + constants.FullMgmt)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
client.SetCookies(resp.Cookies())
|
|
|
|
// call endpoint with session, without credentials, (added to client after previous request)
|
|
resp, err = client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
resp, err = client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Get(baseURL + constants.FullMgmt)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// logout user
|
|
resp, err = client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Post(baseURL + constants.LogoutPath)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// calling endpoint should fail with unauthorized access
|
|
resp, err = client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
})
|
|
|
|
Convey("unauthenticated catalog request", func() {
|
|
client := resty.New()
|
|
|
|
// mgmt should work both unauthenticated and authenticated
|
|
resp, err := client.R().
|
|
Get(baseURL + constants.FullMgmt)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// call endpoint without session
|
|
resp, err = client.R().
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsOpenIDEnabled(t *testing.T) {
|
|
Convey("make oidc server", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
mockOIDCServer, err := test.MockOIDCRun()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
defer func() {
|
|
err := mockOIDCServer.Shutdown()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
|
|
rootDir := t.TempDir()
|
|
|
|
Convey("Only OAuth2 provided", func() {
|
|
mockOIDCConfig := mockOIDCServer.Config()
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
OpenID: &config.OpenIDConfig{
|
|
Providers: map[string]config.OpenIDProviderConfig{
|
|
"github": {
|
|
ClientID: mockOIDCConfig.ClientID,
|
|
ClientSecret: mockOIDCConfig.ClientSecret,
|
|
KeyPath: "",
|
|
Issuer: mockOIDCConfig.Issuer,
|
|
Scopes: []string{"email", "groups"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
ctlr := api.NewController(conf)
|
|
|
|
ctlr.Config.Storage.RootDirectory = rootDir
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
|
|
cm.StartServer()
|
|
defer cm.StopServer()
|
|
test.WaitTillServerReady(baseURL)
|
|
|
|
resp, err := resty.R().
|
|
Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
})
|
|
|
|
Convey("Unsupported provider", func() {
|
|
mockOIDCConfig := mockOIDCServer.Config()
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
OpenID: &config.OpenIDConfig{
|
|
Providers: map[string]config.OpenIDProviderConfig{
|
|
"invalidProvider": {
|
|
ClientID: mockOIDCConfig.ClientID,
|
|
ClientSecret: mockOIDCConfig.ClientSecret,
|
|
KeyPath: "",
|
|
Issuer: mockOIDCConfig.Issuer,
|
|
Scopes: []string{"email", "groups"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
ctlr := api.NewController(conf)
|
|
|
|
ctlr.Config.Storage.RootDirectory = rootDir
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
|
|
cm.StartServer()
|
|
defer cm.StopServer()
|
|
test.WaitTillServerReady(baseURL)
|
|
|
|
// it will work because we have an invalid provider, and no other authn enabled, so no authn enabled
|
|
// normally an invalid provider will exit with error in cli validations
|
|
resp, err := resty.R().
|
|
Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestAuthnSessionErrors(t *testing.T) {
|
|
Convey("make controller", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
defaultVal := true
|
|
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
invalidSessionID := "sessionID"
|
|
|
|
// need a username different than ldap one, to test both logic
|
|
content := fmt.Sprintf("%s:$2y$05$hlbSXDp6hzDLu6VwACS39ORvVRpr3OMR4RlJ31jtlaOEGnPjKZI1m\n", htpasswdUsername)
|
|
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(content)
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
ldapServer := newTestLDAPServer()
|
|
port = test.GetFreePort()
|
|
|
|
ldapPort, err := strconv.Atoi(port)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ldapServer.Start(ldapPort)
|
|
defer ldapServer.Stop()
|
|
|
|
mockOIDCServer, err := test.MockOIDCRun()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
defer func() {
|
|
err := mockOIDCServer.Shutdown()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
|
|
rootDir := t.TempDir()
|
|
|
|
mockOIDCConfig := mockOIDCServer.Config()
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
LDAP: &config.LDAPConfig{
|
|
Insecure: true,
|
|
Address: LDAPAddress,
|
|
Port: ldapPort,
|
|
BindDN: LDAPBindDN,
|
|
BindPassword: LDAPBindPassword,
|
|
BaseDN: LDAPBaseDN,
|
|
UserAttribute: "uid",
|
|
},
|
|
OpenID: &config.OpenIDConfig{
|
|
Providers: map[string]config.OpenIDProviderConfig{
|
|
"oidc": {
|
|
ClientID: mockOIDCConfig.ClientID,
|
|
ClientSecret: mockOIDCConfig.ClientSecret,
|
|
KeyPath: "",
|
|
Issuer: mockOIDCConfig.Issuer,
|
|
Scopes: []string{"email", "groups"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
uiConfig := &extconf.UIConfig{
|
|
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
|
}
|
|
|
|
searchConfig := &extconf.SearchConfig{
|
|
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
|
}
|
|
|
|
conf.Extensions = &extconf.ExtensionConfig{
|
|
UI: uiConfig,
|
|
Search: searchConfig,
|
|
}
|
|
|
|
ctlr := api.NewController(conf)
|
|
|
|
ctlr.Config.Storage.RootDirectory = rootDir
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
|
|
cm.StartServer()
|
|
defer cm.StopServer()
|
|
test.WaitTillServerReady(baseURL)
|
|
|
|
Convey("trigger basic authn middle(htpasswd) error", func() {
|
|
client := resty.New()
|
|
|
|
ctlr.MetaDB = mocks.MetaDBMock{
|
|
SetUserGroupsFn: func(ctx context.Context, groups []string) error {
|
|
return ErrUnexpectedError
|
|
},
|
|
}
|
|
|
|
resp, err := client.R().
|
|
SetBasicAuth(htpasswdUsername, passphrase).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
|
})
|
|
|
|
Convey("trigger basic authn middle(ldap) error", func() {
|
|
client := resty.New()
|
|
|
|
ctlr.MetaDB = mocks.MetaDBMock{
|
|
SetUserGroupsFn: func(ctx context.Context, groups []string) error {
|
|
return ErrUnexpectedError
|
|
},
|
|
}
|
|
|
|
resp, err := client.R().
|
|
SetBasicAuth(username, passphrase).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
|
})
|
|
|
|
Convey("trigger updateUserData error", func() {
|
|
client := resty.New()
|
|
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
|
|
|
|
ctlr.MetaDB = mocks.MetaDBMock{
|
|
SetUserGroupsFn: func(ctx context.Context, groups []string) error {
|
|
return ErrUnexpectedError
|
|
},
|
|
}
|
|
|
|
resp, err := client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
SetQueryParam("provider", "oidc").
|
|
Get(baseURL + constants.LoginPath)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
|
})
|
|
|
|
Convey("trigger session middle metaDB errors", func() {
|
|
client := resty.New()
|
|
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
|
|
|
|
user := mockoidc.DefaultUser()
|
|
user.Groups = []string{"group1", "group2"}
|
|
|
|
mockOIDCServer.QueueUser(user)
|
|
|
|
ctlr.MetaDB = mocks.MetaDBMock{}
|
|
|
|
// first login user
|
|
resp, err := client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
SetQueryParam("provider", "oidc").
|
|
Get(baseURL + constants.LoginPath)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
Convey("trigger session middle error internal server error", func() {
|
|
cookies := resp.Cookies()
|
|
|
|
client.SetCookies(cookies)
|
|
|
|
ctlr.MetaDB = mocks.MetaDBMock{
|
|
GetUserGroupsFn: func(ctx context.Context) ([]string, error) {
|
|
return []string{}, ErrUnexpectedError
|
|
},
|
|
}
|
|
|
|
// call endpoint with session (added to client after previous request)
|
|
resp, err = client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
|
})
|
|
|
|
Convey("trigger session middle error GetUserGroups not found", func() {
|
|
cookies := resp.Cookies()
|
|
|
|
client.SetCookies(cookies)
|
|
|
|
ctlr.MetaDB = mocks.MetaDBMock{
|
|
GetUserGroupsFn: func(ctx context.Context) ([]string, error) {
|
|
return []string{}, errors.ErrUserDataNotFound
|
|
},
|
|
}
|
|
|
|
// call endpoint with session (added to client after previous request)
|
|
resp, err = client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
})
|
|
})
|
|
|
|
Convey("trigger no email error in routes(callback)", func() {
|
|
user := mockoidc.DefaultUser()
|
|
user.Email = ""
|
|
|
|
mockOIDCServer.QueueUser(user)
|
|
|
|
client := resty.New()
|
|
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
|
|
|
|
client.SetCookie(&http.Cookie{Name: "session"})
|
|
|
|
// call endpoint with session (added to client after previous request)
|
|
resp, err := client.R().
|
|
SetQueryParam("provider", "oidc").
|
|
Get(baseURL + constants.LoginPath)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
})
|
|
|
|
Convey("trigger session save error in routes(callback)", func() {
|
|
err := os.Chmod(rootDir, 0o000)
|
|
So(err, ShouldBeNil)
|
|
|
|
defer func() {
|
|
err := os.Chmod(rootDir, storageConstants.DefaultDirPerms)
|
|
So(err, ShouldBeNil)
|
|
}()
|
|
|
|
client := resty.New()
|
|
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
|
|
|
|
// first login user
|
|
resp, err := client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
SetQueryParam("provider", "oidc").
|
|
Get(baseURL + constants.LoginPath)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
|
})
|
|
|
|
Convey("trigger session save error in basicAuthn", func() {
|
|
err := os.Chmod(rootDir, 0o000)
|
|
So(err, ShouldBeNil)
|
|
|
|
defer func() {
|
|
err := os.Chmod(rootDir, storageConstants.DefaultDirPerms)
|
|
So(err, ShouldBeNil)
|
|
}()
|
|
|
|
client := resty.New()
|
|
|
|
// first htpasswd saveSessionLoggedUser() error
|
|
resp, err := client.R().
|
|
SetBasicAuth(htpasswdUsername, passphrase).
|
|
Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
|
|
|
// second ldap saveSessionLoggedUser() error
|
|
resp, err = client.R().
|
|
SetBasicAuth(username, passphrase).
|
|
Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
|
})
|
|
|
|
Convey("trigger session middle errors", func() {
|
|
client := resty.New()
|
|
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
|
|
|
|
user := mockoidc.DefaultUser()
|
|
user.Groups = []string{"group1", "group2"}
|
|
|
|
mockOIDCServer.QueueUser(user)
|
|
|
|
// first login user
|
|
resp, err := client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
SetQueryParam("provider", "oidc").
|
|
Get(baseURL + constants.LoginPath)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
Convey("trigger bad session encoding error in authn", func() {
|
|
cookies := resp.Cookies()
|
|
for _, cookie := range cookies {
|
|
if cookie.Name == "session" {
|
|
cookie.Value = "badSessionValue"
|
|
}
|
|
}
|
|
|
|
client.SetCookies(cookies)
|
|
|
|
// call endpoint with session (added to client after previous request)
|
|
resp, err = client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
})
|
|
|
|
Convey("web request without cookies", func() {
|
|
client.SetCookie(&http.Cookie{})
|
|
|
|
// call endpoint with session (added to client after previous request)
|
|
resp, err = client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
})
|
|
|
|
Convey("web request with userless cookie", func() {
|
|
// first get session
|
|
session, err := ctlr.CookieStore.Get(resp.RawResponse.Request, "session")
|
|
So(err, ShouldBeNil)
|
|
|
|
session.ID = invalidSessionID
|
|
session.IsNew = false
|
|
session.Values["authStatus"] = true
|
|
|
|
cookieStore, ok := ctlr.CookieStore.(*sessions.FilesystemStore)
|
|
So(ok, ShouldBeTrue)
|
|
|
|
// first encode sessionID
|
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.ID,
|
|
cookieStore.Codecs...)
|
|
So(err, ShouldBeNil)
|
|
|
|
// save cookie
|
|
cookie := sessions.NewCookie(session.Name(), encoded, session.Options)
|
|
client.SetCookie(cookie)
|
|
|
|
// encode session values and save on disk
|
|
encoded, err = securecookie.EncodeMulti(session.Name(), session.Values,
|
|
cookieStore.Codecs...)
|
|
So(err, ShouldBeNil)
|
|
|
|
filename := filepath.Join(rootDir, "_sessions", "session_"+session.ID)
|
|
|
|
err = os.WriteFile(filename, []byte(encoded), 0o600)
|
|
So(err, ShouldBeNil)
|
|
|
|
// call endpoint with session (added to client after previous request)
|
|
resp, err = client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
})
|
|
|
|
Convey("web request with authStatus false cookie", func() {
|
|
// first get session
|
|
session, err := ctlr.CookieStore.Get(resp.RawResponse.Request, "session")
|
|
So(err, ShouldBeNil)
|
|
|
|
session.ID = invalidSessionID
|
|
session.IsNew = false
|
|
session.Values["authStatus"] = false
|
|
session.Values["username"] = username
|
|
|
|
cookieStore, ok := ctlr.CookieStore.(*sessions.FilesystemStore)
|
|
So(ok, ShouldBeTrue)
|
|
|
|
// first encode sessionID
|
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.ID,
|
|
cookieStore.Codecs...)
|
|
So(err, ShouldBeNil)
|
|
|
|
// save cookie
|
|
cookie := sessions.NewCookie(session.Name(), encoded, session.Options)
|
|
client.SetCookie(cookie)
|
|
|
|
// encode session values and save on disk
|
|
encoded, err = securecookie.EncodeMulti(session.Name(), session.Values,
|
|
cookieStore.Codecs...)
|
|
So(err, ShouldBeNil)
|
|
|
|
filename := filepath.Join(rootDir, "_sessions", "session_"+session.ID)
|
|
|
|
err = os.WriteFile(filename, []byte(encoded), 0o600)
|
|
So(err, ShouldBeNil)
|
|
|
|
// call endpoint with session (added to client after previous request)
|
|
resp, err = client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestAuthnMetaDBErrors(t *testing.T) {
|
|
Convey("make controller", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
htpasswdPath := test.MakeHtpasswdFile()
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
mockOIDCServer, err := test.MockOIDCRun()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
defer func() {
|
|
err := mockOIDCServer.Shutdown()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
|
|
rootDir := t.TempDir()
|
|
|
|
mockOIDCConfig := mockOIDCServer.Config()
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
OpenID: &config.OpenIDConfig{
|
|
Providers: map[string]config.OpenIDProviderConfig{
|
|
"oidc": {
|
|
ClientID: mockOIDCConfig.ClientID,
|
|
ClientSecret: mockOIDCConfig.ClientSecret,
|
|
KeyPath: "",
|
|
Issuer: mockOIDCConfig.Issuer,
|
|
Scopes: []string{"openid", "email"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
ctlr := api.NewController(conf)
|
|
|
|
ctlr.Config.Storage.RootDirectory = rootDir
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
|
|
cm.StartServer()
|
|
defer cm.StopServer()
|
|
test.WaitTillServerReady(baseURL)
|
|
|
|
Convey("trigger basic authn middle(htpasswd) error", func() {
|
|
client := resty.New()
|
|
|
|
ctlr.MetaDB = mocks.MetaDBMock{
|
|
SetUserGroupsFn: func(ctx context.Context, groups []string) error {
|
|
return ErrUnexpectedError
|
|
},
|
|
}
|
|
|
|
resp, err := client.R().
|
|
SetBasicAuth(username, passphrase).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
|
})
|
|
|
|
Convey("trigger session middle metaDB errors", func() {
|
|
client := resty.New()
|
|
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
|
|
|
|
user := mockoidc.DefaultUser()
|
|
user.Groups = []string{"group1", "group2"}
|
|
|
|
mockOIDCServer.QueueUser(user)
|
|
|
|
// first login user
|
|
resp, err := client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
SetQueryParam("provider", "oidc").
|
|
Get(baseURL + constants.LoginPath)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
Convey("trigger session middle error", func() {
|
|
cookies := resp.Cookies()
|
|
|
|
client.SetCookies(cookies)
|
|
|
|
ctlr.MetaDB = mocks.MetaDBMock{
|
|
GetUserGroupsFn: func(ctx context.Context) ([]string, error) {
|
|
return []string{}, ErrUnexpectedError
|
|
},
|
|
}
|
|
|
|
// call endpoint with session (added to client after previous request)
|
|
resp, err = client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestAuthorization(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
htpasswdPath := test.MakeHtpasswdFile()
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Repositories: config.Repositories{
|
|
AuthorizationAllRepos: config.PolicyGroup{
|
|
Policies: []config.Policy{
|
|
{
|
|
Users: []string{},
|
|
Actions: []string{},
|
|
},
|
|
},
|
|
DefaultPolicy: []string{},
|
|
},
|
|
},
|
|
AdminPolicy: config.Policy{
|
|
Users: []string{},
|
|
Actions: []string{},
|
|
},
|
|
}
|
|
|
|
Convey("with openid", func() {
|
|
mockOIDCServer, err := test.MockOIDCRun()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
defer func() {
|
|
err := mockOIDCServer.Shutdown()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
|
|
mockOIDCConfig := mockOIDCServer.Config()
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
OpenID: &config.OpenIDConfig{
|
|
Providers: map[string]config.OpenIDProviderConfig{
|
|
"oidc": {
|
|
ClientID: mockOIDCConfig.ClientID,
|
|
ClientSecret: mockOIDCConfig.ClientSecret,
|
|
KeyPath: "",
|
|
Issuer: mockOIDCConfig.Issuer,
|
|
Scopes: []string{"openid", "email"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
ctlr := api.NewController(conf)
|
|
ctlr.Config.Storage.RootDirectory = t.TempDir()
|
|
|
|
err = test.WriteImageToFileSystem(test.CreateDefaultImage(), "zot-test", "0.0.1",
|
|
test.GetDefaultStoreController(ctlr.Config.Storage.RootDirectory, ctlr.Log))
|
|
So(err, ShouldBeNil)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
client := resty.New()
|
|
|
|
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
|
|
|
|
mockOIDCServer.QueueUser(&mockoidc.MockUser{
|
|
Email: "test",
|
|
Subject: "1234567890",
|
|
})
|
|
|
|
// first login user
|
|
resp, err := client.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
SetQueryParam("provider", "oidc").
|
|
Get(baseURL + constants.LoginPath)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
client.SetCookies(resp.Cookies())
|
|
client.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
|
|
|
|
RunAuthorizationTests(t, client, baseURL, conf)
|
|
})
|
|
|
|
Convey("with basic auth", func() {
|
|
ctlr := api.NewController(conf)
|
|
ctlr.Config.Storage.RootDirectory = t.TempDir()
|
|
|
|
err := test.WriteImageToFileSystem(test.CreateDefaultImage(), "zot-test", "0.0.1",
|
|
test.GetDefaultStoreController(ctlr.Config.Storage.RootDirectory, ctlr.Log))
|
|
So(err, ShouldBeNil)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
client := resty.New()
|
|
client.SetBasicAuth(username, passphrase)
|
|
|
|
RunAuthorizationTests(t, client, baseURL, conf)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestGetUsername(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(getCredString(username, passphrase))
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
ctlr := makeController(conf, dir)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
resp, err := resty.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
// test base64 encode
|
|
resp, err = resty.R().SetHeader("Authorization", "Basic should fail").Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
// test "username:password" encoding
|
|
resp, err = resty.R().SetHeader("Authorization", "Basic dGVzdA==").Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
// failed parsing authorization header
|
|
resp, err = resty.R().SetHeader("Authorization", "Basic ").Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
var e apiErr.Error
|
|
err = json.Unmarshal(resp.Body(), &e)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
})
|
|
}
|
|
|
|
func TestAuthorizationWithOnlyAnonymousPolicy(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
const TestRepo = "my-repos/repo"
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
conf.HTTP.Auth = &config.AuthConfig{}
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Repositories: config.Repositories{
|
|
TestRepo: config.PolicyGroup{
|
|
AnonymousPolicy: []string{"read"},
|
|
},
|
|
},
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
ctlr := makeController(conf, dir)
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
blob := []byte("hello, blob!")
|
|
digest := godigest.FromBytes(blob).String()
|
|
|
|
resp, err := resty.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
resp, err = resty.R().Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
var e apiErr.Error
|
|
err = json.Unmarshal(resp.Body(), &e)
|
|
So(err, ShouldBeNil)
|
|
|
|
// should get 401 without create
|
|
resp, err = resty.R().Post(baseURL + "/v2/" + TestRepo + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
if entry, ok := conf.HTTP.AccessControl.Repositories[TestRepo]; ok {
|
|
entry.AnonymousPolicy = []string{"create", "read"}
|
|
conf.HTTP.AccessControl.Repositories[TestRepo] = entry
|
|
}
|
|
|
|
// now it should get 202
|
|
resp, err = resty.R().Post(baseURL + "/v2/" + TestRepo + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc := resp.Header().Get("Location")
|
|
|
|
// uploading blob should get 201
|
|
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)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
cblob, cdigest := test.GetRandomImageConfig()
|
|
|
|
resp, err = resty.R().Post(baseURL + "/v2/" + TestRepo + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc = test.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)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
manifest := ispec.Manifest{
|
|
Config: ispec.Descriptor{
|
|
MediaType: "application/vnd.oci.image.config.v1+json",
|
|
Digest: cdigest,
|
|
Size: int64(len(cblob)),
|
|
},
|
|
Layers: []ispec.Descriptor{
|
|
{
|
|
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
|
Digest: godigest.FromBytes(blob),
|
|
Size: int64(len(blob)),
|
|
},
|
|
},
|
|
}
|
|
manifest.SchemaVersion = 2
|
|
manifestBlob, err := json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().
|
|
SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
|
|
SetBody(manifestBlob).
|
|
Put(baseURL + "/v2/" + TestRepo + "/manifests/0.0.2")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
updateBlob := []byte("Hello, blob update!")
|
|
|
|
resp, err = resty.R().
|
|
Post(baseURL + "/v2/" + TestRepo + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc = test.Location(baseURL, resp)
|
|
// uploading blob should get 201
|
|
resp, err = resty.R().
|
|
SetHeader("Content-Length", fmt.Sprintf("%d", len(updateBlob))).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetQueryParam("digest", string(godigest.FromBytes(updateBlob))).
|
|
SetBody(updateBlob).
|
|
Put(loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
updatedManifest := ispec.Manifest{
|
|
Config: ispec.Descriptor{
|
|
MediaType: "application/vnd.oci.image.config.v1+json",
|
|
Digest: cdigest,
|
|
Size: int64(len(cblob)),
|
|
},
|
|
Layers: []ispec.Descriptor{
|
|
{
|
|
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
|
Digest: godigest.FromBytes(updateBlob),
|
|
Size: int64(len(updateBlob)),
|
|
},
|
|
},
|
|
}
|
|
updatedManifest.SchemaVersion = 2
|
|
updatedManifestBlob, err := json.Marshal(updatedManifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
// update manifest should get 401 without update perm
|
|
resp, err = resty.R().SetBody(updatedManifestBlob).
|
|
Put(baseURL + "/v2/" + TestRepo + "/manifests/0.0.2")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
// get the manifest and check if it's the old one
|
|
resp, err = resty.R().
|
|
Get(baseURL + "/v2/" + TestRepo + "/manifests/0.0.2")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
So(resp.Body(), ShouldResemble, manifestBlob)
|
|
|
|
// add update perm on repo
|
|
if entry, ok := conf.HTTP.AccessControl.Repositories[TestRepo]; ok {
|
|
entry.AnonymousPolicy = []string{"create", "read", "update"}
|
|
conf.HTTP.AccessControl.Repositories[TestRepo] = entry
|
|
}
|
|
|
|
// update manifest should get 201 with update perm
|
|
resp, err = resty.R().
|
|
SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
|
|
SetBody(updatedManifestBlob).
|
|
Put(baseURL + "/v2/" + TestRepo + "/manifests/0.0.2")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
// get the manifest and check if it's the new updated one
|
|
resp, err = resty.R().
|
|
Get(baseURL + "/v2/" + TestRepo + "/manifests/0.0.2")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
So(resp.Body(), ShouldResemble, updatedManifestBlob)
|
|
|
|
resp, err = resty.R().Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// make sure anonymous is correctly handled when using acCtx (requestcontext package)
|
|
catalog := struct {
|
|
Repositories []string `json:"repositories"`
|
|
}{}
|
|
|
|
err = json.Unmarshal(resp.Body(), &catalog)
|
|
So(err, ShouldBeNil)
|
|
So(len(catalog.Repositories), ShouldEqual, 1)
|
|
So(catalog.Repositories, ShouldContain, TestRepo)
|
|
|
|
err = os.Mkdir(path.Join(dir, "zot-test"), storageConstants.DefaultDirPerms)
|
|
So(err, ShouldBeNil)
|
|
|
|
err = test.WriteImageToFileSystem(test.CreateDefaultImage(), "zot-test", "tag", ctlr.StoreController)
|
|
So(err, ShouldBeNil)
|
|
|
|
// should not have read rights on zot-test
|
|
resp, err = resty.R().Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
err = json.Unmarshal(resp.Body(), &catalog)
|
|
So(err, ShouldBeNil)
|
|
So(len(catalog.Repositories), ShouldEqual, 1)
|
|
So(catalog.Repositories, ShouldContain, TestRepo)
|
|
|
|
// add rights
|
|
conf.HTTP.AccessControl.Repositories["zot-test"] = config.PolicyGroup{
|
|
AnonymousPolicy: []string{"read"},
|
|
}
|
|
|
|
resp, err = resty.R().Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
err = json.Unmarshal(resp.Body(), &catalog)
|
|
So(err, ShouldBeNil)
|
|
So(len(catalog.Repositories), ShouldEqual, 2)
|
|
So(catalog.Repositories, ShouldContain, TestRepo)
|
|
So(catalog.Repositories, ShouldContain, "zot-test")
|
|
})
|
|
}
|
|
|
|
func TestAuthorizationWithAnonymousPolicyBasicAuthAndSessionHeader(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
const TestRepo = "my-repos/repo"
|
|
const AllRepos = "**"
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
badpassphrase := "bad"
|
|
htpasswdContent := fmt.Sprintf("%s:$2y$05$hlbSXDp6hzDLu6VwACS39ORvVRpr3OMR4RlJ31jtlaOEGnPjKZI1m\n",
|
|
htpasswdUsername)
|
|
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(htpasswdContent)
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
img := test.CreateRandomImage()
|
|
tagAnonymous := "1.0-anon"
|
|
tagAuth := "1.0-auth"
|
|
tagUnauth := "1.0-unauth"
|
|
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Repositories: config.Repositories{
|
|
AllRepos: config.PolicyGroup{
|
|
Policies: []config.Policy{
|
|
{
|
|
Users: []string{htpasswdUsername},
|
|
Actions: []string{"read"},
|
|
},
|
|
},
|
|
AnonymousPolicy: []string{"read"},
|
|
},
|
|
},
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
ctlr := makeController(conf, dir)
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// /v2 access
|
|
// Can access /v2 without credentials
|
|
resp, err := resty.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// Can access /v2 without credentials and with X-Zot-Api-Client=zot-ui
|
|
resp, err = resty.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// Can access /v2 with correct credentials
|
|
resp, err = resty.R().
|
|
SetBasicAuth(htpasswdUsername, passphrase).
|
|
Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// Fail to access /v2 with incorrect credentials
|
|
resp, err = resty.R().
|
|
SetBasicAuth(htpasswdUsername, badpassphrase).
|
|
Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
// Catalog access
|
|
resp, err = resty.R().Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
var apiError apiErr.Error
|
|
err = json.Unmarshal(resp.Body(), &apiError)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
apiError = apiErr.Error{}
|
|
err = json.Unmarshal(resp.Body(), &apiError)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().
|
|
SetBasicAuth(htpasswdUsername, passphrase).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
apiError = apiErr.Error{}
|
|
err = json.Unmarshal(resp.Body(), &apiError)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().
|
|
SetBasicAuth(htpasswdUsername, badpassphrase).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
apiError = apiErr.Error{}
|
|
err = json.Unmarshal(resp.Body(), &apiError)
|
|
So(err, ShouldBeNil)
|
|
|
|
// upload capability
|
|
// should get 403 without create
|
|
err = test.UploadImage(img, baseURL, TestRepo, tagAnonymous)
|
|
So(err, ShouldNotBeNil)
|
|
|
|
err = test.UploadImageWithBasicAuth(img, baseURL,
|
|
TestRepo, tagAuth, htpasswdUsername, passphrase)
|
|
So(err, ShouldNotBeNil)
|
|
|
|
err = test.UploadImageWithBasicAuth(img, baseURL,
|
|
TestRepo, tagUnauth, htpasswdUsername, badpassphrase)
|
|
So(err, ShouldNotBeNil)
|
|
|
|
if entry, ok := conf.HTTP.AccessControl.Repositories[AllRepos]; ok {
|
|
entry.AnonymousPolicy = []string{"create", "read"}
|
|
entry.Policies[0] = config.Policy{
|
|
Users: []string{htpasswdUsername},
|
|
Actions: []string{"create", "read"},
|
|
}
|
|
conf.HTTP.AccessControl.Repositories[AllRepos] = entry
|
|
}
|
|
|
|
// now it should succeed for valid users
|
|
err = test.UploadImage(img, baseURL, TestRepo, tagAnonymous)
|
|
So(err, ShouldBeNil)
|
|
|
|
err = test.UploadImageWithBasicAuth(img, baseURL,
|
|
TestRepo, tagAuth, htpasswdUsername, passphrase)
|
|
So(err, ShouldBeNil)
|
|
|
|
err = test.UploadImageWithBasicAuth(img, baseURL,
|
|
TestRepo, tagUnauth, htpasswdUsername, badpassphrase)
|
|
So(err, ShouldNotBeNil)
|
|
|
|
// read capability
|
|
catalog := struct {
|
|
Repositories []string `json:"repositories"`
|
|
}{}
|
|
|
|
resp, err = resty.R().Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
err = json.Unmarshal(resp.Body(), &catalog)
|
|
So(err, ShouldBeNil)
|
|
So(len(catalog.Repositories), ShouldEqual, 1)
|
|
So(catalog.Repositories, ShouldContain, TestRepo)
|
|
|
|
catalog = struct {
|
|
Repositories []string `json:"repositories"`
|
|
}{}
|
|
|
|
resp, err = resty.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
err = json.Unmarshal(resp.Body(), &catalog)
|
|
So(err, ShouldBeNil)
|
|
So(len(catalog.Repositories), ShouldEqual, 1)
|
|
So(catalog.Repositories, ShouldContain, TestRepo)
|
|
|
|
catalog = struct {
|
|
Repositories []string `json:"repositories"`
|
|
}{}
|
|
|
|
resp, err = resty.R().
|
|
SetBasicAuth(htpasswdUsername, passphrase).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
err = json.Unmarshal(resp.Body(), &catalog)
|
|
So(err, ShouldBeNil)
|
|
So(len(catalog.Repositories), ShouldEqual, 1)
|
|
So(catalog.Repositories, ShouldContain, TestRepo)
|
|
|
|
resp, err = resty.R().
|
|
SetBasicAuth(htpasswdUsername, badpassphrase).
|
|
Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
})
|
|
}
|
|
|
|
func TestAuthorizationWithMultiplePolicies(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
// have two users: "test" user for user Policy, and "bob" for default policy
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(getCredString(username, passphrase) +
|
|
"\n" + getCredString("bob", passphrase))
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
// config with all policy types, to test that the correct one is applied in each case
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Repositories: config.Repositories{
|
|
AuthorizationAllRepos: config.PolicyGroup{
|
|
Policies: []config.Policy{
|
|
{
|
|
Users: []string{},
|
|
Actions: []string{},
|
|
},
|
|
},
|
|
DefaultPolicy: []string{},
|
|
AnonymousPolicy: []string{},
|
|
},
|
|
},
|
|
AdminPolicy: config.Policy{
|
|
Users: []string{},
|
|
Actions: []string{},
|
|
},
|
|
}
|
|
|
|
Convey("with openid", func() {
|
|
dir := t.TempDir()
|
|
|
|
mockOIDCServer, err := test.MockOIDCRun()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
defer func() {
|
|
err := mockOIDCServer.Shutdown()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
|
|
mockOIDCConfig := mockOIDCServer.Config()
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
OpenID: &config.OpenIDConfig{
|
|
Providers: map[string]config.OpenIDProviderConfig{
|
|
"oidc": {
|
|
ClientID: mockOIDCConfig.ClientID,
|
|
ClientSecret: mockOIDCConfig.ClientSecret,
|
|
KeyPath: "",
|
|
Issuer: mockOIDCConfig.Issuer,
|
|
Scopes: []string{"openid", "email"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
ctlr := api.NewController(conf)
|
|
ctlr.Config.Storage.RootDirectory = dir
|
|
|
|
err = test.WriteImageToFileSystem(test.CreateDefaultImage(), "zot-test", "0.0.1",
|
|
test.GetDefaultStoreController(ctlr.Config.Storage.RootDirectory, ctlr.Log))
|
|
So(err, ShouldBeNil)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
testUserClient := resty.New()
|
|
|
|
testUserClient.SetRedirectPolicy(test.CustomRedirectPolicy(20))
|
|
|
|
mockOIDCServer.QueueUser(&mockoidc.MockUser{
|
|
Email: "test",
|
|
Subject: "1234567890",
|
|
})
|
|
|
|
// first login user
|
|
resp, err := testUserClient.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
SetQueryParam("provider", "oidc").
|
|
Get(baseURL + constants.LoginPath)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
testUserClient.SetCookies(resp.Cookies())
|
|
testUserClient.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
|
|
|
|
bobUserClient := resty.New()
|
|
|
|
bobUserClient.SetRedirectPolicy(test.CustomRedirectPolicy(20))
|
|
|
|
mockOIDCServer.QueueUser(&mockoidc.MockUser{
|
|
Email: "bob",
|
|
Subject: "1234567890",
|
|
})
|
|
|
|
// first login user
|
|
resp, err = bobUserClient.R().
|
|
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
|
|
SetQueryParam("provider", "oidc").
|
|
Get(baseURL + constants.LoginPath)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
bobUserClient.SetCookies(resp.Cookies())
|
|
bobUserClient.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
|
|
|
|
RunAuthorizationWithMultiplePoliciesTests(t, testUserClient, bobUserClient, baseURL, conf)
|
|
})
|
|
|
|
Convey("with basic auth", func() {
|
|
dir := t.TempDir()
|
|
|
|
ctlr := api.NewController(conf)
|
|
ctlr.Config.Storage.RootDirectory = dir
|
|
|
|
err := test.WriteImageToFileSystem(test.CreateDefaultImage(), "zot-test", "0.0.1",
|
|
test.GetDefaultStoreController(ctlr.Config.Storage.RootDirectory, ctlr.Log))
|
|
So(err, ShouldBeNil)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
testUserClient := resty.New()
|
|
testUserClient.SetBasicAuth(username, passphrase)
|
|
|
|
bobUserClient := resty.New()
|
|
bobUserClient.SetBasicAuth("bob", passphrase)
|
|
|
|
RunAuthorizationWithMultiplePoliciesTests(t, testUserClient, bobUserClient, baseURL, conf)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestInvalidCases(t *testing.T) {
|
|
Convey("Invalid repo dir", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(getCredString(username, passphrase))
|
|
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
ctlr := makeController(conf, dir)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer func(ctrl *api.Controller) {
|
|
err := os.Chmod(dir, 0o755)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
err = ctrl.Server.Shutdown(context.Background())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
err = os.RemoveAll(ctrl.Config.Storage.RootDirectory)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}(ctlr)
|
|
|
|
err := os.Chmod(dir, 0o000)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
digest := test.GetTestBlobDigest("zot-cve-test", "config").String()
|
|
name := "zot-c-test"
|
|
|
|
client := resty.New()
|
|
|
|
params := make(map[string]string)
|
|
params["from"] = "zot-cveid-test"
|
|
params["mount"] = digest
|
|
|
|
postResponse, err := client.R().
|
|
SetBasicAuth(username, passphrase).SetQueryParams(params).
|
|
Post(fmt.Sprintf("%s/v2/%s/blobs/uploads/", baseURL, name))
|
|
So(err, ShouldBeNil)
|
|
So(postResponse.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
|
})
|
|
}
|
|
|
|
func TestHTTPReadOnly(t *testing.T) {
|
|
Convey("Single cred", t, func() {
|
|
singleCredtests := []string{}
|
|
user := ALICE
|
|
password := ALICE
|
|
singleCredtests = append(singleCredtests, getCredString(user, password))
|
|
singleCredtests = append(singleCredtests, getCredString(user, password)+"\n")
|
|
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
for _, testString := range singleCredtests {
|
|
func() {
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
// enable read-only mode
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Repositories: config.Repositories{
|
|
AuthorizationAllRepos: config.PolicyGroup{
|
|
DefaultPolicy: []string{"read"},
|
|
},
|
|
},
|
|
}
|
|
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(testString)
|
|
defer os.Remove(htpasswdPath)
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// with creds, should get expected status code
|
|
resp, _ := resty.R().SetBasicAuth(user, password).Get(baseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// with creds, any modifications should still fail on read-only mode
|
|
resp, err := resty.R().SetBasicAuth(user, password).
|
|
Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
// with invalid creds, it should fail
|
|
resp, _ = resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/")
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
}()
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCrossRepoMount(t *testing.T) {
|
|
Convey("Cross Repo Mount", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(getCredString(username, passphrase))
|
|
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
ctlr := api.NewController(conf)
|
|
|
|
ctlr.Config.Storage.RootDirectory = dir
|
|
ctlr.Config.Storage.RemoteCache = false
|
|
ctlr.Config.Storage.Dedupe = false
|
|
|
|
err := test.WriteImageToFileSystem(test.CreateDefaultImage(), "zot-cve-test", "test", storage.StoreController{
|
|
DefaultStore: test.GetDefaultImageStore(dir, ctlr.Log),
|
|
})
|
|
So(err, ShouldBeNil)
|
|
|
|
cm := test.NewControllerManager(ctlr) //nolint: varnamelen
|
|
cm.StartAndWait(port)
|
|
|
|
params := make(map[string]string)
|
|
|
|
var manifestDigest godigest.Digest
|
|
manifestDigest, _, _ = test.GetOciLayoutDigests(path.Join(dir, "zot-cve-test"))
|
|
|
|
dgst := manifestDigest
|
|
name := "zot-cve-test"
|
|
params["mount"] = string(manifestDigest)
|
|
params["from"] = name
|
|
|
|
client := resty.New()
|
|
headResponse, err := client.R().SetBasicAuth(username, passphrase).
|
|
Head(fmt.Sprintf("%s/v2/%s/blobs/%s", baseURL, name, manifestDigest))
|
|
So(err, ShouldBeNil)
|
|
So(headResponse.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// All invalid request of mount should return 202.
|
|
params["mount"] = "sha:"
|
|
|
|
postResponse, err := client.R().
|
|
SetBasicAuth(username, passphrase).SetQueryParams(params).
|
|
Post(baseURL + "/v2/zot-c-test/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
location, err := postResponse.RawResponse.Location()
|
|
So(err, ShouldBeNil)
|
|
So(location.String(), ShouldStartWith, fmt.Sprintf("%s%s/zot-c-test/%s/%s",
|
|
baseURL, constants.RoutePrefix, constants.Blobs, constants.Uploads))
|
|
|
|
incorrectParams := make(map[string]string)
|
|
incorrectParams["mount"] = test.GetTestBlobDigest("zot-cve-test", "manifest").String()
|
|
incorrectParams["from"] = "zot-x-test"
|
|
|
|
postResponse, err = client.R().
|
|
SetBasicAuth(username, passphrase).SetQueryParams(incorrectParams).
|
|
Post(baseURL + "/v2/zot-y-test/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
So(test.Location(baseURL, postResponse), ShouldStartWith, fmt.Sprintf("%s%s/zot-y-test/%s/%s",
|
|
baseURL, constants.RoutePrefix, constants.Blobs, constants.Uploads))
|
|
|
|
// Use correct request
|
|
// This is correct request but it will return 202 because blob is not present in cache.
|
|
params["mount"] = string(manifestDigest)
|
|
postResponse, err = client.R().
|
|
SetBasicAuth(username, passphrase).SetQueryParams(params).
|
|
Post(baseURL + "/v2/zot-c-test/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
So(test.Location(baseURL, postResponse), ShouldStartWith, fmt.Sprintf("%s%s/zot-c-test/%s/%s",
|
|
baseURL, constants.RoutePrefix, constants.Blobs, constants.Uploads))
|
|
|
|
// Send same request again
|
|
postResponse, err = client.R().
|
|
SetBasicAuth(username, passphrase).SetQueryParams(params).
|
|
Post(baseURL + "/v2/zot-c-test/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
|
|
// Valid requests
|
|
postResponse, err = client.R().
|
|
SetBasicAuth(username, passphrase).SetQueryParams(params).
|
|
Post(baseURL + "/v2/zot-d-test/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
|
|
headResponse, err = client.R().SetBasicAuth(username, passphrase).
|
|
Head(fmt.Sprintf("%s/v2/zot-cv-test/blobs/%s", baseURL, manifestDigest))
|
|
So(err, ShouldBeNil)
|
|
So(headResponse.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
postResponse, err = client.R().
|
|
SetBasicAuth(username, passphrase).SetQueryParams(params).Post(baseURL + "/v2/zot-c-test/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
|
|
postResponse, err = client.R().
|
|
SetBasicAuth(username, passphrase).SetQueryParams(params).
|
|
Post(baseURL + "/v2/ /blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(postResponse.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
blob := manifestDigest.Encoded()
|
|
|
|
buf, err := os.ReadFile(path.Join(ctlr.Config.Storage.RootDirectory, "zot-cve-test/blobs/sha256/"+blob))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
postResponse, err = client.R().SetHeader("Content-type", "application/octet-stream").
|
|
SetBasicAuth(username, passphrase).SetQueryParam("digest", "sha256:"+blob).
|
|
SetBody(buf).Post(baseURL + "/v2/zot-d-test/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(postResponse.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
// We have uploaded a blob and since we have provided digest it should be full blob upload and there should be entry
|
|
// in cache, now try mount blob request status and it should be 201 because now blob is present in cache
|
|
// and it should do hard link.
|
|
|
|
// make a new server with dedupe on and same rootDir (can't restart because of metadb - boltdb being open)
|
|
newDir := t.TempDir()
|
|
err = test.CopyFiles(dir, newDir)
|
|
So(err, ShouldBeNil)
|
|
|
|
cm.StopServer()
|
|
|
|
ctlr.Config.Storage.Dedupe = true
|
|
ctlr.Config.Storage.GC = false
|
|
ctlr.Config.Storage.RootDirectory = newDir
|
|
cm = test.NewControllerManager(ctlr) //nolint: varnamelen
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// wait for dedupe task to run
|
|
time.Sleep(10 * time.Second)
|
|
|
|
params["mount"] = string(manifestDigest)
|
|
postResponse, err = client.R().
|
|
SetBasicAuth(username, passphrase).SetQueryParams(params).
|
|
Post(baseURL + "/v2/zot-mount-test/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(postResponse.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
So(test.Location(baseURL, postResponse), ShouldEqual, fmt.Sprintf("%s%s/zot-mount-test/%s/%s:%s",
|
|
baseURL, constants.RoutePrefix, constants.Blobs, godigest.SHA256, blob))
|
|
|
|
// Check os.SameFile here
|
|
cachePath := path.Join(ctlr.Config.Storage.RootDirectory, "zot-d-test", "blobs/sha256", dgst.Encoded())
|
|
|
|
cacheFi, err := os.Stat(cachePath)
|
|
So(err, ShouldBeNil)
|
|
|
|
linkPath := path.Join(ctlr.Config.Storage.RootDirectory, "zot-mount-test", "blobs/sha256", dgst.Encoded())
|
|
|
|
linkFi, err := os.Stat(linkPath)
|
|
So(err, ShouldBeNil)
|
|
|
|
So(os.SameFile(cacheFi, linkFi), ShouldEqual, true)
|
|
|
|
// Now try another mount request and this time it should be from above uploaded repo i.e zot-mount-test
|
|
// mount request should pass and should return 201.
|
|
params["mount"] = string(manifestDigest)
|
|
params["from"] = "zot-mount-test"
|
|
postResponse, err = client.R().
|
|
SetBasicAuth(username, passphrase).SetQueryParams(params).
|
|
Post(baseURL + "/v2/zot-mount1-test/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(postResponse.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
So(test.Location(baseURL, postResponse), ShouldEqual, fmt.Sprintf("%s%s/zot-mount1-test/%s/%s:%s",
|
|
baseURL, constants.RoutePrefix, constants.Blobs, godigest.SHA256, blob))
|
|
|
|
linkPath = path.Join(ctlr.Config.Storage.RootDirectory, "zot-mount1-test", "blobs/sha256", dgst.Encoded())
|
|
|
|
linkFi, err = os.Stat(linkPath)
|
|
So(err, ShouldBeNil)
|
|
|
|
So(os.SameFile(cacheFi, linkFi), ShouldEqual, true)
|
|
|
|
headResponse, err = client.R().SetBasicAuth(username, passphrase).
|
|
Head(fmt.Sprintf("%s/v2/zot-cv-test/blobs/%s", baseURL, manifestDigest))
|
|
So(err, ShouldBeNil)
|
|
So(headResponse.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// Invalid request
|
|
params = make(map[string]string)
|
|
params["mount"] = "sha256:"
|
|
postResponse, err = client.R().
|
|
SetBasicAuth(username, passphrase).SetQueryParams(params).
|
|
Post(baseURL + "/v2/zot-mount-test/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
|
|
params = make(map[string]string)
|
|
params["from"] = "zot-cve-test"
|
|
postResponse, err = client.R().
|
|
SetBasicAuth(username, passphrase).SetQueryParams(params).
|
|
Post(baseURL + "/v2/zot-mount-test/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(postResponse.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
|
|
})
|
|
|
|
Convey("Disable dedupe and cache", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(getCredString(username, passphrase))
|
|
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
|
|
ctlr := api.NewController(conf)
|
|
ctlr.Config.Storage.RootDirectory = dir
|
|
ctlr.Config.Storage.Dedupe = false
|
|
ctlr.Config.Storage.GC = false
|
|
|
|
image := test.CreateImageWith().RandomLayers(1, 10).DefaultConfig().Build()
|
|
|
|
err := test.WriteImageToFileSystem(image, "zot-cve-test", "0.0.1",
|
|
test.GetDefaultStoreController(dir, ctlr.Log))
|
|
So(err, ShouldBeNil)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// digest := test.GetTestBlobDigest("zot-cve-test", "layer").String()
|
|
digest := godigest.FromBytes(image.Layers[0])
|
|
name := "zot-c-test"
|
|
client := resty.New()
|
|
headResponse, err := client.R().SetBasicAuth(username, passphrase).
|
|
Head(fmt.Sprintf("%s/v2/%s/blobs/%s", baseURL, name, digest))
|
|
So(err, ShouldBeNil)
|
|
So(headResponse.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
})
|
|
}
|
|
|
|
func TestParallelRequests(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
srcImageName string
|
|
srcImageTag string
|
|
destImageName string
|
|
destImageTag string
|
|
testCaseName string
|
|
}{
|
|
{
|
|
srcImageName: "zot-test",
|
|
srcImageTag: "0.0.1",
|
|
destImageName: "zot-1-test",
|
|
destImageTag: "0.0.1",
|
|
testCaseName: "Request-1",
|
|
},
|
|
{
|
|
srcImageName: "zot-test",
|
|
srcImageTag: "0.0.1",
|
|
destImageName: "zot-2-test",
|
|
testCaseName: "Request-2",
|
|
},
|
|
{
|
|
srcImageName: "zot-cve-test",
|
|
srcImageTag: "0.0.1",
|
|
destImageName: "a/zot-3-test",
|
|
testCaseName: "Request-3",
|
|
},
|
|
{
|
|
srcImageName: "zot-cve-test",
|
|
srcImageTag: "0.0.1",
|
|
destImageName: "b/zot-4-test",
|
|
testCaseName: "Request-4",
|
|
},
|
|
{
|
|
srcImageName: "zot-cve-test",
|
|
srcImageTag: "0.0.1",
|
|
destImageName: "zot-5-test",
|
|
testCaseName: "Request-5",
|
|
},
|
|
{
|
|
srcImageName: "zot-cve-test",
|
|
srcImageTag: "0.0.1",
|
|
destImageName: "zot-1-test",
|
|
testCaseName: "Request-6",
|
|
},
|
|
{
|
|
srcImageName: "zot-cve-test",
|
|
srcImageTag: "0.0.1",
|
|
destImageName: "zot-2-test",
|
|
testCaseName: "Request-7",
|
|
},
|
|
{
|
|
srcImageName: "zot-cve-test",
|
|
srcImageTag: "0.0.1",
|
|
destImageName: "zot-3-test",
|
|
testCaseName: "Request-8",
|
|
},
|
|
{
|
|
srcImageName: "zot-cve-test",
|
|
srcImageTag: "0.0.1",
|
|
destImageName: "zot-4-test",
|
|
testCaseName: "Request-9",
|
|
},
|
|
{
|
|
srcImageName: "zot-cve-test",
|
|
srcImageTag: "0.0.1",
|
|
destImageName: "zot-5-test",
|
|
testCaseName: "Request-10",
|
|
},
|
|
{
|
|
srcImageName: "zot-test",
|
|
srcImageTag: "0.0.1",
|
|
destImageName: "zot-1-test",
|
|
destImageTag: "0.0.1",
|
|
testCaseName: "Request-11",
|
|
},
|
|
{
|
|
srcImageName: "zot-test",
|
|
srcImageTag: "0.0.1",
|
|
destImageName: "zot-2-test",
|
|
testCaseName: "Request-12",
|
|
},
|
|
{
|
|
srcImageName: "zot-cve-test",
|
|
srcImageTag: "0.0.1",
|
|
destImageName: "a/zot-3-test",
|
|
testCaseName: "Request-13",
|
|
},
|
|
{
|
|
srcImageName: "zot-cve-test",
|
|
srcImageTag: "0.0.1",
|
|
destImageName: "b/zot-4-test",
|
|
testCaseName: "Request-14",
|
|
},
|
|
}
|
|
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(getCredString(username, passphrase))
|
|
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
firstSubDir := t.TempDir()
|
|
secondSubDir := t.TempDir()
|
|
|
|
subPaths := make(map[string]config.StorageConfig)
|
|
|
|
subPaths["/a"] = config.StorageConfig{RootDirectory: firstSubDir}
|
|
subPaths["/b"] = config.StorageConfig{RootDirectory: secondSubDir}
|
|
|
|
ctlr := makeController(conf, dir)
|
|
ctlr.Config.Storage.SubPaths = subPaths
|
|
|
|
testImagesDir := t.TempDir()
|
|
testImagesController := test.GetDefaultStoreController(testImagesDir, ctlr.Log)
|
|
|
|
err := test.WriteImageToFileSystem(test.CreateRandomImage(), "zot-test", "0.0.1", testImagesController)
|
|
assert.Equal(t, err, nil, "Error should be nil")
|
|
|
|
err = test.WriteImageToFileSystem(test.CreateRandomImage(), "zot-cve-test", "0.0.1", testImagesController)
|
|
assert.Equal(t, err, nil, "Error should be nil")
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
|
|
// without creds, should get access error
|
|
for i, testcase := range testCases {
|
|
testcase := testcase
|
|
run := i
|
|
|
|
t.Run(testcase.testCaseName, func(t *testing.T) {
|
|
t.Parallel()
|
|
client := resty.New()
|
|
|
|
tagResponse, err := client.R().SetBasicAuth(username, passphrase).
|
|
Get(baseURL + "/v2/" + testcase.destImageName + "/tags/list")
|
|
assert.Equal(t, err, nil, "Error should be nil")
|
|
assert.NotEqual(t, tagResponse.StatusCode(), http.StatusBadRequest, "bad request")
|
|
|
|
manifestList := getAllManifests(path.Join(testImagesDir, testcase.srcImageName))
|
|
|
|
for _, manifest := range manifestList {
|
|
headResponse, err := client.R().SetBasicAuth(username, passphrase).
|
|
Head(baseURL + "/v2/" + testcase.destImageName + "/manifests/" + manifest)
|
|
assert.Equal(t, err, nil, "Error should be nil")
|
|
assert.Equal(t, headResponse.StatusCode(), http.StatusNotFound, "response status code should return 404")
|
|
|
|
getResponse, err := client.R().SetBasicAuth(username, passphrase).
|
|
Get(baseURL + "/v2/" + testcase.destImageName + "/manifests/" + manifest)
|
|
assert.Equal(t, err, nil, "Error should be nil")
|
|
assert.Equal(t, getResponse.StatusCode(), http.StatusNotFound, "response status code should return 404")
|
|
}
|
|
|
|
blobList := getAllBlobs(path.Join(testImagesDir, testcase.srcImageName))
|
|
|
|
for _, blob := range blobList {
|
|
// Get request of blob
|
|
headResponse, err := client.R().
|
|
SetBasicAuth(username, passphrase).
|
|
Head(baseURL + "/v2/" + testcase.destImageName + "/blobs/sha256:" + blob)
|
|
|
|
assert.Equal(t, err, nil, "Should not be nil")
|
|
assert.NotEqual(t, headResponse.StatusCode(), http.StatusInternalServerError,
|
|
"internal server error should not occurred")
|
|
|
|
getResponse, err := client.R().
|
|
SetBasicAuth(username, passphrase).
|
|
Get(baseURL + "/v2/" + testcase.destImageName + "/blobs/sha256:" + blob)
|
|
|
|
assert.Equal(t, err, nil, "Should not be nil")
|
|
assert.NotEqual(t, getResponse.StatusCode(), http.StatusInternalServerError,
|
|
"internal server error should not occurred")
|
|
|
|
blobPath := path.Join(testImagesDir, testcase.srcImageName, "blobs/sha256", blob)
|
|
|
|
buf, err := os.ReadFile(blobPath)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Post request of blob
|
|
postResponse, err := client.R().
|
|
SetHeader("Content-type", "application/octet-stream").
|
|
SetBasicAuth(username, passphrase).
|
|
SetBody(buf).Post(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/")
|
|
|
|
assert.Equal(t, err, nil, "Error should be nil")
|
|
assert.NotEqual(t, postResponse.StatusCode(), http.StatusInternalServerError,
|
|
"response status code should not return 500")
|
|
|
|
// Post request with query parameter
|
|
if run%2 == 0 {
|
|
postResponse, err = client.R().
|
|
SetHeader("Content-type", "application/octet-stream").
|
|
SetBasicAuth(username, passphrase).
|
|
SetBody(buf).
|
|
Post(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/")
|
|
|
|
assert.Equal(t, err, nil, "Error should be nil")
|
|
assert.NotEqual(t, postResponse.StatusCode(), http.StatusInternalServerError,
|
|
"response status code should not return 500")
|
|
|
|
var sessionID string
|
|
sessionIDList := postResponse.Header().Values("Blob-Upload-UUID")
|
|
if len(sessionIDList) == 0 {
|
|
location := postResponse.Header().Values("Location")
|
|
firstLocation := location[0]
|
|
splitLocation := strings.Split(firstLocation, "/")
|
|
sessionID = splitLocation[len(splitLocation)-1]
|
|
} else {
|
|
sessionID = sessionIDList[0]
|
|
}
|
|
|
|
file, err := os.Open(blobPath)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
reader := bufio.NewReader(file)
|
|
|
|
buf := make([]byte, 5*1024*1024)
|
|
|
|
if run%4 == 0 {
|
|
readContent := 0
|
|
for {
|
|
nbytes, err := reader.Read(buf)
|
|
if err != nil {
|
|
if goerrors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
panic(err)
|
|
}
|
|
// Patch request of blob
|
|
|
|
patchResponse, err := client.R().
|
|
SetBody(buf[0:nbytes]).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetHeader("Content-Length", fmt.Sprintf("%d", nbytes)).
|
|
SetHeader("Content-Range", fmt.Sprintf("%d", readContent)+"-"+fmt.Sprintf("%d", readContent+nbytes-1)).
|
|
SetBasicAuth(username, passphrase).
|
|
Patch(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/" + sessionID)
|
|
|
|
assert.Equal(t, err, nil, "Error should be nil")
|
|
assert.NotEqual(t, patchResponse.StatusCode(), http.StatusInternalServerError,
|
|
"response status code should not return 500")
|
|
|
|
readContent += nbytes
|
|
}
|
|
} else {
|
|
for {
|
|
nbytes, err := reader.Read(buf)
|
|
if err != nil {
|
|
if goerrors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
panic(err)
|
|
}
|
|
// Patch request of blob
|
|
|
|
patchResponse, err := client.R().SetBody(buf[0:nbytes]).SetHeader("Content-type", "application/octet-stream").
|
|
SetBasicAuth(username, passphrase).
|
|
Patch(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/" + sessionID)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
assert.Equal(t, err, nil, "Error should be nil")
|
|
assert.NotEqual(t, patchResponse.StatusCode(), http.StatusInternalServerError,
|
|
"response status code should not return 500")
|
|
}
|
|
}
|
|
} else {
|
|
postResponse, err = client.R().
|
|
SetHeader("Content-type", "application/octet-stream").
|
|
SetBasicAuth(username, passphrase).
|
|
SetBody(buf).SetQueryParam("digest", "sha256:"+blob).
|
|
Post(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/")
|
|
|
|
assert.Equal(t, err, nil, "Error should be nil")
|
|
assert.NotEqual(t, postResponse.StatusCode(), http.StatusInternalServerError,
|
|
"response status code should not return 500")
|
|
}
|
|
|
|
headResponse, err = client.R().
|
|
SetBasicAuth(username, passphrase).
|
|
Head(baseURL + "/v2/" + testcase.destImageName + "/blobs/sha256:" + blob)
|
|
|
|
assert.Equal(t, err, nil, "Should not be nil")
|
|
assert.NotEqual(t, headResponse.StatusCode(), http.StatusInternalServerError, "response should return success code")
|
|
|
|
getResponse, err = client.R().
|
|
SetBasicAuth(username, passphrase).
|
|
Get(baseURL + "/v2/" + testcase.destImageName + "/blobs/sha256:" + blob)
|
|
|
|
assert.Equal(t, err, nil, "Should not be nil")
|
|
assert.NotEqual(t, getResponse.StatusCode(), http.StatusInternalServerError, "response should return success code")
|
|
}
|
|
|
|
tagResponse, err = client.R().SetBasicAuth(username, passphrase).
|
|
Get(baseURL + "/v2/" + testcase.destImageName + "/tags/list")
|
|
assert.Equal(t, err, nil, "Error should be nil")
|
|
assert.Equal(t, tagResponse.StatusCode(), http.StatusOK, "response status code should return success code")
|
|
|
|
repoResponse, err := client.R().SetBasicAuth(username, passphrase).
|
|
Get(baseURL + constants.RoutePrefix + constants.ExtCatalogPrefix)
|
|
assert.Equal(t, err, nil, "Error should be nil")
|
|
assert.Equal(t, repoResponse.StatusCode(), http.StatusOK, "response status code should return success code")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHardLink(t *testing.T) {
|
|
Convey("Validate hard link", t, func() {
|
|
port := test.GetFreePort()
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
dir := t.TempDir()
|
|
|
|
err := os.Chmod(dir, 0o400)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
subDir := t.TempDir()
|
|
|
|
err = os.Chmod(subDir, 0o400)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ctlr := makeController(conf, dir)
|
|
subPaths := make(map[string]config.StorageConfig)
|
|
|
|
subPaths["/a"] = config.StorageConfig{RootDirectory: subDir, Dedupe: true}
|
|
ctlr.Config.Storage.SubPaths = subPaths
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
err = os.Chmod(dir, 0o644)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
err = os.Chmod(subDir, 0o644)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
So(ctlr.Config.Storage.Dedupe, ShouldEqual, false)
|
|
})
|
|
}
|
|
|
|
func TestImageSignatures(t *testing.T) {
|
|
Convey("Validate signatures", t, func() {
|
|
// start a new server
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
dir := t.TempDir()
|
|
ctlr := makeController(conf, dir)
|
|
cm := test.NewControllerManager(ctlr)
|
|
// this blocks
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
repoName := "signed-repo"
|
|
img := test.CreateRandomImage()
|
|
content := img.ManifestDescriptor.Data
|
|
digest := img.ManifestDescriptor.Digest
|
|
|
|
err := test.UploadImage(img, baseURL, repoName, "1.0")
|
|
So(err, ShouldBeNil)
|
|
|
|
Convey("Validate cosign signatures", func() {
|
|
cwd, err := os.Getwd()
|
|
So(err, ShouldBeNil)
|
|
defer func() { _ = os.Chdir(cwd) }()
|
|
tdir := t.TempDir()
|
|
_ = os.Chdir(tdir)
|
|
|
|
// generate a keypair
|
|
os.Setenv("COSIGN_PASSWORD", "")
|
|
err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil)
|
|
So(err, ShouldBeNil)
|
|
|
|
annotations := []string{"tag=1.0"}
|
|
|
|
// sign the image
|
|
err = sign.SignCmd(&options.RootOptions{Verbose: true, Timeout: 1 * time.Minute},
|
|
options.KeyOpts{KeyRef: path.Join(tdir, "cosign.key"), PassFunc: generate.GetPass},
|
|
options.SignOptions{
|
|
Registry: options.RegistryOptions{AllowInsecure: true},
|
|
AnnotationOptions: options.AnnotationOptions{Annotations: annotations},
|
|
Upload: true,
|
|
},
|
|
[]string{fmt.Sprintf("localhost:%s/%s@%s", port, repoName, digest.String())})
|
|
So(err, ShouldBeNil)
|
|
|
|
// verify the image
|
|
aopts := &options.AnnotationOptions{Annotations: annotations}
|
|
amap, err := aopts.AnnotationsMap()
|
|
So(err, ShouldBeNil)
|
|
vrfy := verify.VerifyCommand{
|
|
RegistryOptions: options.RegistryOptions{AllowInsecure: true},
|
|
CheckClaims: true,
|
|
KeyRef: path.Join(tdir, "cosign.pub"),
|
|
Annotations: amap,
|
|
IgnoreTlog: true,
|
|
}
|
|
err = vrfy.Exec(context.TODO(), []string{fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")})
|
|
So(err, ShouldBeNil)
|
|
|
|
// verify the image with incorrect tag
|
|
aopts = &options.AnnotationOptions{Annotations: []string{"tag=2.0"}}
|
|
amap, err = aopts.AnnotationsMap()
|
|
So(err, ShouldBeNil)
|
|
vrfy = verify.VerifyCommand{
|
|
RegistryOptions: options.RegistryOptions{AllowInsecure: true},
|
|
CheckClaims: true,
|
|
KeyRef: path.Join(tdir, "cosign.pub"),
|
|
Annotations: amap,
|
|
IgnoreTlog: true,
|
|
}
|
|
err = vrfy.Exec(context.TODO(), []string{fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")})
|
|
So(err, ShouldNotBeNil)
|
|
|
|
// verify the image with incorrect key
|
|
aopts = &options.AnnotationOptions{Annotations: []string{"tag=1.0"}}
|
|
amap, err = aopts.AnnotationsMap()
|
|
So(err, ShouldBeNil)
|
|
vrfy = verify.VerifyCommand{
|
|
CheckClaims: true,
|
|
RegistryOptions: options.RegistryOptions{AllowInsecure: true},
|
|
KeyRef: path.Join(tdir, "cosign.key"),
|
|
Annotations: amap,
|
|
IgnoreTlog: true,
|
|
}
|
|
err = vrfy.Exec(context.TODO(), []string{fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")})
|
|
So(err, ShouldNotBeNil)
|
|
|
|
// generate another keypair
|
|
err = os.Remove(path.Join(tdir, "cosign.pub"))
|
|
So(err, ShouldBeNil)
|
|
err = os.Remove(path.Join(tdir, "cosign.key"))
|
|
So(err, ShouldBeNil)
|
|
|
|
os.Setenv("COSIGN_PASSWORD", "")
|
|
err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil)
|
|
So(err, ShouldBeNil)
|
|
|
|
// verify the image with incorrect key
|
|
aopts = &options.AnnotationOptions{Annotations: []string{"tag=1.0"}}
|
|
amap, err = aopts.AnnotationsMap()
|
|
So(err, ShouldBeNil)
|
|
vrfy = verify.VerifyCommand{
|
|
CheckClaims: true,
|
|
RegistryOptions: options.RegistryOptions{AllowInsecure: true},
|
|
KeyRef: path.Join(tdir, "cosign.pub"),
|
|
Annotations: amap,
|
|
IgnoreTlog: true,
|
|
}
|
|
err = vrfy.Exec(context.TODO(), []string{fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")})
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Validate notation signatures", func() {
|
|
cwd, err := os.Getwd()
|
|
So(err, ShouldBeNil)
|
|
defer func() { _ = os.Chdir(cwd) }()
|
|
tdir := t.TempDir()
|
|
_ = os.Chdir(tdir)
|
|
|
|
test.NotationPathLock.Lock()
|
|
defer test.NotationPathLock.Unlock()
|
|
|
|
test.LoadNotationPath(tdir)
|
|
|
|
err = test.GenerateNotationCerts(tdir, "good")
|
|
So(err, ShouldBeNil)
|
|
|
|
err = test.GenerateNotationCerts(tdir, "bad")
|
|
So(err, ShouldBeNil)
|
|
|
|
image := fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")
|
|
err = test.SignWithNotation("good", image, tdir)
|
|
So(err, ShouldBeNil)
|
|
|
|
err = test.VerifyWithNotation(image, tdir)
|
|
So(err, ShouldBeNil)
|
|
|
|
// check list
|
|
sigs, err := test.ListNotarySignatures(image, tdir)
|
|
So(len(sigs), ShouldEqual, 1)
|
|
So(err, ShouldBeNil)
|
|
|
|
// check unsupported manifest media type
|
|
resp, err := resty.R().SetHeader("Content-Type", "application/vnd.unsupported.image.manifest.v1+json").
|
|
SetBody(content).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnsupportedMediaType)
|
|
|
|
// check invalid content with artifact media type
|
|
resp, err = resty.R().SetHeader("Content-Type", artifactspec.MediaTypeArtifactManifest).
|
|
SetBody([]byte("bogus")).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
|
|
Convey("Validate corrupted signature", func() {
|
|
// verify with corrupted signature
|
|
resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
|
|
fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
var refs ispec.Index
|
|
err = json.Unmarshal(resp.Body(), &refs)
|
|
So(err, ShouldBeNil)
|
|
So(len(refs.Manifests), ShouldEqual, 1)
|
|
err = os.WriteFile(path.Join(dir, repoName, "blobs",
|
|
strings.ReplaceAll(refs.Manifests[0].Digest.String(), ":", "/")), []byte("corrupt"), 0o600)
|
|
So(err, ShouldBeNil)
|
|
resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
|
|
fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
|
|
|
err = test.VerifyWithNotation(image, tdir)
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Validate deleted signature", func() {
|
|
// verify with corrupted signature
|
|
resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
|
|
fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
var refs ispec.Index
|
|
err = json.Unmarshal(resp.Body(), &refs)
|
|
So(err, ShouldBeNil)
|
|
So(len(refs.Manifests), ShouldEqual, 1)
|
|
err = os.Remove(path.Join(dir, repoName, "blobs",
|
|
strings.ReplaceAll(refs.Manifests[0].Digest.String(), ":", "/")))
|
|
So(err, ShouldBeNil)
|
|
resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
|
|
fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
err = test.VerifyWithNotation(image, tdir)
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
})
|
|
|
|
Convey("GetOrasReferrers", func() {
|
|
// cover error paths
|
|
resp, err := resty.R().Get(
|
|
fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, "badRepo", "badDigest"))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
resp, err = resty.R().Get(
|
|
fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, "badDigest"))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
|
|
resp, err = resty.R().Get(
|
|
fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
resp, err = resty.R().SetQueryParam("artifactType", "badArtifact").Get(
|
|
fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
|
|
fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, "badRepo", digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestManifestValidation(t *testing.T) {
|
|
Convey("Validate manifest", t, func() {
|
|
// start a new server
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
dir := t.TempDir()
|
|
ctlr := makeController(conf, dir)
|
|
cm := test.NewControllerManager(ctlr)
|
|
// this blocks
|
|
cm.StartServer()
|
|
time.Sleep(1000 * time.Millisecond)
|
|
defer cm.StopServer()
|
|
|
|
repoName := "validation"
|
|
blobContent := []byte("this is a blob")
|
|
blobDigest := godigest.FromBytes(blobContent)
|
|
So(blobDigest, ShouldNotBeNil)
|
|
|
|
img := test.CreateRandomImage()
|
|
content := img.ManifestDescriptor.Data
|
|
digest := img.ManifestDescriptor.Digest
|
|
configDigest := img.ConfigDescriptor.Digest
|
|
configBlob := img.ConfigDescriptor.Data
|
|
|
|
err := test.UploadImage(img, baseURL, repoName, "1.0")
|
|
So(err, ShouldBeNil)
|
|
|
|
Convey("empty layers should pass validation", func() {
|
|
// create a manifest
|
|
manifest := ispec.Manifest{
|
|
Config: ispec.Descriptor{
|
|
MediaType: ispec.MediaTypeImageConfig,
|
|
Size: int64(len(configBlob)),
|
|
Digest: configDigest,
|
|
},
|
|
Layers: []ispec.Descriptor{},
|
|
Annotations: map[string]string{
|
|
"key": "val",
|
|
},
|
|
}
|
|
manifest.SchemaVersion = 2
|
|
|
|
mcontent, err := json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
})
|
|
|
|
Convey("empty layers and schemaVersion missing should fail validation", func() {
|
|
// create a manifest
|
|
manifest := ispec.Manifest{
|
|
Config: ispec.Descriptor{
|
|
MediaType: ispec.MediaTypeImageConfig,
|
|
Size: int64(len(configBlob)),
|
|
Digest: configDigest,
|
|
},
|
|
Layers: []ispec.Descriptor{},
|
|
Annotations: map[string]string{
|
|
"key": "val",
|
|
},
|
|
}
|
|
|
|
mcontent, err := json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
})
|
|
|
|
Convey("missing layer should fail validation", func() {
|
|
missingLayer := []byte("missing layer")
|
|
missingLayerDigest := godigest.FromBytes(missingLayer)
|
|
// create a manifest
|
|
manifest := ispec.Manifest{
|
|
Config: ispec.Descriptor{
|
|
MediaType: ispec.MediaTypeImageConfig,
|
|
Size: int64(len(configBlob)),
|
|
Digest: configDigest,
|
|
},
|
|
Layers: []ispec.Descriptor{
|
|
{
|
|
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
|
Digest: digest,
|
|
Size: int64(len(content)),
|
|
},
|
|
{
|
|
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
|
Digest: missingLayerDigest,
|
|
Size: int64(len(missingLayer)),
|
|
},
|
|
},
|
|
Annotations: map[string]string{
|
|
"key": "val",
|
|
},
|
|
}
|
|
manifest.SchemaVersion = 2
|
|
|
|
mcontent, err := json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
})
|
|
|
|
Convey("wrong mediatype should fail validation", func() {
|
|
// create a manifest
|
|
manifest := ispec.Manifest{
|
|
MediaType: "bad.mediatype",
|
|
Config: ispec.Descriptor{
|
|
MediaType: ispec.MediaTypeImageConfig,
|
|
Size: int64(len(configBlob)),
|
|
Digest: configDigest,
|
|
},
|
|
Layers: []ispec.Descriptor{
|
|
{
|
|
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
|
Digest: digest,
|
|
Size: int64(len(content)),
|
|
},
|
|
},
|
|
Annotations: map[string]string{
|
|
"key": "val",
|
|
},
|
|
}
|
|
manifest.SchemaVersion = 2
|
|
|
|
mcontent, err := json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
})
|
|
|
|
Convey("multiarch image should pass validation", func() {
|
|
index := ispec.Index{
|
|
MediaType: ispec.MediaTypeImageIndex,
|
|
Manifests: []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: digest,
|
|
Size: int64(len((content))),
|
|
},
|
|
},
|
|
}
|
|
|
|
index.SchemaVersion = 2
|
|
|
|
indexContent, err := json.Marshal(index)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
SetBody(indexContent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/index", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
})
|
|
|
|
Convey("multiarch image without schemaVersion should fail validation", func() {
|
|
index := ispec.Index{
|
|
MediaType: ispec.MediaTypeImageIndex,
|
|
Manifests: []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: digest,
|
|
Size: int64(len((content))),
|
|
},
|
|
},
|
|
}
|
|
|
|
indexContent, err := json.Marshal(index)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
SetBody(indexContent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/index", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
})
|
|
|
|
Convey("multiarch image with missing manifest should fail validation", func() {
|
|
index := ispec.Index{
|
|
MediaType: ispec.MediaTypeImageIndex,
|
|
Manifests: []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: digest,
|
|
Size: int64(len((content))),
|
|
},
|
|
{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: godigest.FromString("missing layer"),
|
|
Size: 10,
|
|
},
|
|
},
|
|
}
|
|
|
|
index.SchemaVersion = 2
|
|
|
|
indexContent, err := json.Marshal(index)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
SetBody(indexContent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/index", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestArtifactReferences(t *testing.T) {
|
|
Convey("Validate Artifact References", t, func() {
|
|
// start a new server
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
dir := t.TempDir()
|
|
ctlr := makeController(conf, dir)
|
|
cm := test.NewControllerManager(ctlr)
|
|
// this blocks
|
|
cm.StartServer()
|
|
time.Sleep(1000 * time.Millisecond)
|
|
defer cm.StopServer()
|
|
|
|
repoName := "artifact-repo"
|
|
content := []byte("this is a blob")
|
|
digest := godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
|
|
cfg, layers, manifest, err := test.GetImageComponents(2) //nolint:staticcheck
|
|
So(err, ShouldBeNil)
|
|
|
|
err = test.UploadImage(
|
|
test.Image{
|
|
Config: cfg,
|
|
Layers: layers,
|
|
Manifest: manifest,
|
|
}, baseURL, repoName, "1.0")
|
|
So(err, ShouldBeNil)
|
|
|
|
content, err = json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
digest = godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
|
|
artifactType := "application/vnd.example.icecream.v1"
|
|
|
|
Convey("Validate Image Manifest Reference", func() {
|
|
resp, err := resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
var referrers ispec.Index
|
|
err = json.Unmarshal(resp.Body(), &referrers)
|
|
So(err, ShouldBeNil)
|
|
So(referrers.Manifests, ShouldBeEmpty)
|
|
|
|
// now upload a reference
|
|
|
|
// upload image config blob
|
|
resp, err = resty.R().Post(baseURL + fmt.Sprintf("/v2/%s/blobs/uploads/", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc := test.Location(baseURL, resp)
|
|
cblob, cdigest := test.GetEmptyImageConfig()
|
|
|
|
resp, err = resty.R().
|
|
SetContentLength(true).
|
|
SetHeader("Content-Length", fmt.Sprintf("%d", len(cblob))).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetQueryParam("digest", cdigest.String()).
|
|
SetBody(cblob).
|
|
Put(loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
// create a manifest
|
|
manifest := ispec.Manifest{
|
|
Config: ispec.Descriptor{
|
|
MediaType: artifactType,
|
|
Digest: cdigest,
|
|
Size: int64(len(cblob)),
|
|
},
|
|
Layers: []ispec.Descriptor{
|
|
{
|
|
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
|
Digest: digest,
|
|
Size: int64(len(content)),
|
|
},
|
|
},
|
|
Subject: &ispec.Descriptor{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: digest,
|
|
Size: int64(len(content)),
|
|
},
|
|
Annotations: map[string]string{
|
|
"key": "val",
|
|
},
|
|
}
|
|
manifest.SchemaVersion = 2
|
|
|
|
Convey("Using invalid content", func() {
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody([]byte("invalid data")).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
|
|
// unknown repo will return status not found
|
|
resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", "unknown", digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
resp, err = resty.R().SetQueryParams(map[string]string{"artifactType": artifactType}).
|
|
Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// create a bad manifest (constructed manually)
|
|
content := `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:71dbae9d7e6445fb5e0b11328e941b8e8937fdd52465079f536ce44bb78796ed","size":406}}` //nolint: lll
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody(content).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
|
|
// missing layers
|
|
mcontent := []byte("this is a missing blob")
|
|
digest = godigest.FromBytes(mcontent)
|
|
So(digest, ShouldNotBeNil)
|
|
|
|
manifest.Layers = append(manifest.Layers, ispec.Descriptor{
|
|
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
|
Digest: digest,
|
|
Size: int64(len(mcontent)),
|
|
})
|
|
|
|
mcontent, err = json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
// invalid schema version
|
|
manifest.SchemaVersion = 1
|
|
|
|
mcontent, err = json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
|
|
// upload image config blob
|
|
resp, err = resty.R().Post(baseURL + fmt.Sprintf("/v2/%s/blobs/uploads/", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc := test.Location(baseURL, resp)
|
|
cblob = []byte("{}")
|
|
cdigest = godigest.FromBytes(cblob)
|
|
So(cdigest, ShouldNotBeNil)
|
|
|
|
resp, err = resty.R().
|
|
SetContentLength(true).
|
|
SetHeader("Content-Length", fmt.Sprintf("%d", len(cblob))).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetQueryParam("digest", cdigest.String()).
|
|
SetBody(cblob).
|
|
Put(loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
manifest := ispec.Manifest{
|
|
Config: ispec.Descriptor{
|
|
MediaType: "application/vnd.oci.image.config.v1+json",
|
|
Digest: cdigest,
|
|
Size: int64(len(cblob)),
|
|
},
|
|
}
|
|
|
|
manifest.SchemaVersion = 2
|
|
mcontent, err = json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
digest = godigest.FromBytes(mcontent)
|
|
So(digest, ShouldNotBeNil)
|
|
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
|
|
// missing layers
|
|
mcontent = []byte("this is a missing blob")
|
|
digest = godigest.FromBytes(mcontent)
|
|
So(digest, ShouldNotBeNil)
|
|
|
|
manifest.Layers = append(manifest.Layers, ispec.Descriptor{
|
|
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
|
Digest: digest,
|
|
Size: int64(len(mcontent)),
|
|
})
|
|
|
|
mcontent, err = json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
// should fail because config is of type image and blob is not uploaded
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
|
|
// no layers at all
|
|
manifest.Layers = []ispec.Descriptor{}
|
|
|
|
mcontent, err = json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
// should not fail
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
})
|
|
|
|
Convey("Using valid content", func() {
|
|
content, err = json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody(content).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
resp, err = resty.R().SetQueryParams(map[string]string{"artifact": "invalid"}).
|
|
Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
resp, err = resty.R().SetQueryParams(map[string]string{"artifactType": "invalid"}).
|
|
Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
resp, err = resty.R().SetQueryParams(map[string]string{"artifactType": artifactType}).
|
|
Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
So(resp.Header().Get("Content-Type"), ShouldEqual, ispec.MediaTypeImageIndex)
|
|
So(resp.Header().Get("OCI-Filters-Applied"), ShouldEqual, "artifactType")
|
|
|
|
resp, err = resty.R().SetQueryParams(map[string]string{"artifactType": artifactType +
|
|
",otherArtType"}).Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName,
|
|
digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
So(resp.Header().Get("Content-Type"), ShouldEqual, ispec.MediaTypeImageIndex)
|
|
So(resp.Header().Get("OCI-Filters-Applied"), ShouldEqual, "artifactType")
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
//nolint:dupl // duplicated test code
|
|
func TestRouteFailures(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
ctlr.Config.Storage.Commit = true
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
rthdlr := api.NewRouteHandler(ctlr)
|
|
|
|
// NOTE: the url or method itself doesn't matter below since we are calling the handlers directly,
|
|
// so path routing is bypassed
|
|
|
|
Convey("List tags", func() {
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
mux.SetURLVars(request, map[string]string{})
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.ListTags(response, request)
|
|
|
|
resp := response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.ListTags(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
qparm := request.URL.Query()
|
|
qparm.Add("n", "a")
|
|
request.URL.RawQuery = qparm.Encode()
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.ListTags(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
qparm = request.URL.Query()
|
|
qparm.Add("n", "abc")
|
|
request.URL.RawQuery = qparm.Encode()
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.ListTags(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
qparm = request.URL.Query()
|
|
qparm.Add("n", "a")
|
|
qparm.Add("n", "abc")
|
|
request.URL.RawQuery = qparm.Encode()
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.ListTags(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
qparm = request.URL.Query()
|
|
qparm.Add("n", "0")
|
|
request.URL.RawQuery = qparm.Encode()
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.ListTags(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
qparm = request.URL.Query()
|
|
qparm.Add("n", "1")
|
|
qparm.Add("last", "")
|
|
request.URL.RawQuery = qparm.Encode()
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.ListTags(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
qparm = request.URL.Query()
|
|
qparm.Add("n", "1")
|
|
qparm.Add("last", "a")
|
|
request.URL.RawQuery = qparm.Encode()
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.ListTags(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
qparm = request.URL.Query()
|
|
qparm.Add("n", "1")
|
|
qparm.Add("last", "a")
|
|
qparm.Add("last", "abc")
|
|
request.URL.RawQuery = qparm.Encode()
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.ListTags(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
|
|
})
|
|
|
|
Convey("Check manifest", func() {
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodHead, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{})
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.CheckManifest(response, request)
|
|
|
|
resp := response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodHead, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.CheckManifest(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodHead, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo", "reference": ""})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.CheckManifest(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
})
|
|
|
|
Convey("Get manifest", func() {
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{})
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.GetManifest(response, request)
|
|
|
|
resp := response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.GetManifest(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo", "reference": ""})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.GetManifest(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
})
|
|
|
|
Convey("Update manifest", func() {
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{})
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.UpdateManifest(response, request)
|
|
|
|
resp := response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.UpdateManifest(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo", "reference": ""})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.UpdateManifest(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
})
|
|
|
|
Convey("Delete manifest", func() {
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{})
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.DeleteManifest(response, request)
|
|
|
|
resp := response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.DeleteManifest(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo", "reference": ""})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.DeleteManifest(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
})
|
|
|
|
Convey("Check blob", func() {
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodHead, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{})
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.CheckBlob(response, request)
|
|
|
|
resp := response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodHead, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.CheckBlob(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodHead, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo", "digest": ""})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.CheckBlob(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
})
|
|
|
|
Convey("Get blob", func() {
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{})
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.GetBlob(response, request)
|
|
|
|
resp := response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.GetBlob(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo", "digest": ""})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.GetBlob(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
})
|
|
|
|
Convey("Delete blob", func() {
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{})
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.DeleteBlob(response, request)
|
|
|
|
resp := response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.DeleteBlob(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo", "digest": ""})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.DeleteBlob(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
})
|
|
|
|
Convey("Create blob upload", func() {
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPost, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{})
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.CreateBlobUpload(response, request)
|
|
|
|
resp := response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPost, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
qparm := request.URL.Query()
|
|
qparm.Add("mount", "a")
|
|
qparm.Add("mount", "abc")
|
|
request.URL.RawQuery = qparm.Encode()
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.CreateBlobUpload(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPost, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
qparm = request.URL.Query()
|
|
qparm.Add("mount", "a")
|
|
request.URL.RawQuery = qparm.Encode()
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.CreateBlobUpload(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusAccepted)
|
|
})
|
|
|
|
Convey("Get blob upload", func() {
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{})
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.GetBlobUpload(response, request)
|
|
|
|
resp := response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.GetBlobUpload(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
})
|
|
|
|
Convey("Patch blob upload", func() {
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPatch, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{})
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.PatchBlobUpload(response, request)
|
|
|
|
resp := response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.PatchBlobUpload(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
})
|
|
|
|
Convey("Update blob upload", func() {
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{})
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.UpdateBlobUpload(response, request)
|
|
|
|
resp := response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.UpdateBlobUpload(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo", "session_id": "bar"})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.UpdateBlobUpload(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo", "session_id": "bar"})
|
|
qparm := request.URL.Query()
|
|
qparm.Add("digest", "a")
|
|
qparm.Add("digest", "abc")
|
|
request.URL.RawQuery = qparm.Encode()
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.UpdateBlobUpload(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
|
|
})
|
|
|
|
Convey("Delete blob upload", func() {
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{})
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.DeleteBlobUpload(response, request)
|
|
|
|
resp := response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.DeleteBlobUpload(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
})
|
|
|
|
Convey("Get referrers", func() {
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{})
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.GetOrasReferrers(response, request)
|
|
|
|
resp := response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
|
|
|
|
request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "foo"})
|
|
response = httptest.NewRecorder()
|
|
|
|
rthdlr.GetOrasReferrers(response, request)
|
|
|
|
resp = response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestStorageCommit(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
dir := t.TempDir()
|
|
ctlr := makeController(conf, dir)
|
|
ctlr.Config.Storage.Commit = true
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
Convey("Manifests", func() {
|
|
_, _ = Print("\nManifests")
|
|
|
|
cfg, layers, manifest, err := test.GetImageComponents(2) //nolint:staticcheck
|
|
So(err, ShouldBeNil)
|
|
|
|
content := []byte("this is a blob5")
|
|
digest := godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
// check a non-existent manifest
|
|
resp, err := resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
|
|
SetBody(content).Head(baseURL + "/v2/unknown/manifests/test:1.0")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
repoName := "repo7"
|
|
err = test.UploadImage(
|
|
test.Image{
|
|
Config: cfg,
|
|
Layers: layers,
|
|
Manifest: manifest,
|
|
}, baseURL, repoName, "test:1.0")
|
|
So(err, ShouldBeNil)
|
|
|
|
_, err = os.Stat(path.Join(dir, "repo7"))
|
|
So(err, ShouldBeNil)
|
|
|
|
content, err = json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
digest = godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
|
|
SetBody(content).Put(baseURL + "/v2/repo7/manifests/test:1.0")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
digestHdr := resp.Header().Get(constants.DistContentDigestKey)
|
|
So(digestHdr, ShouldNotBeEmpty)
|
|
So(digestHdr, ShouldEqual, digest.String())
|
|
|
|
resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
|
|
SetBody(content).Put(baseURL + "/v2/repo7/manifests/test:1.0.1")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
digestHdr = resp.Header().Get(constants.DistContentDigestKey)
|
|
So(digestHdr, ShouldNotBeEmpty)
|
|
So(digestHdr, ShouldEqual, digest.String())
|
|
|
|
content, err = json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
err = test.UploadImage(
|
|
test.Image{
|
|
Config: cfg,
|
|
Layers: layers,
|
|
Manifest: manifest,
|
|
}, baseURL, repoName, "test:1.0.1")
|
|
So(err, ShouldBeNil)
|
|
|
|
cfg, layers, manifest, err = test.GetImageComponents(1) //nolint:staticcheck
|
|
So(err, ShouldBeNil)
|
|
|
|
err = test.UploadImage(
|
|
test.Image{
|
|
Config: cfg,
|
|
Layers: layers,
|
|
Manifest: manifest,
|
|
}, baseURL, repoName, "test:2.0")
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
|
|
SetBody(content).Put(baseURL + "/v2/repo7/manifests/test:2.0")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
digestHdr = resp.Header().Get(constants.DistContentDigestKey)
|
|
So(digestHdr, ShouldNotBeEmpty)
|
|
So(digestHdr, ShouldEqual, digest.String())
|
|
|
|
// check/get by tag
|
|
resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/test:1.0")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
|
|
resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/test:1.0")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
So(resp.Body(), ShouldNotBeEmpty)
|
|
// check/get by reference
|
|
resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/" + digest.String())
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
|
|
resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/" + digest.String())
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
So(resp.Body(), ShouldNotBeEmpty)
|
|
|
|
// delete manifest by tag should pass
|
|
resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/test:1.0")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
// delete manifest by digest (1.0 deleted but 1.0.1 has same reference)
|
|
resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String())
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
// delete manifest by digest
|
|
resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String())
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
// delete again should fail
|
|
resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String())
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
// check/get by tag
|
|
resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/test:1.0")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/test:1.0")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
So(resp.Body(), ShouldNotBeEmpty)
|
|
resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/test:2.0")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/test:2.0")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
So(resp.Body(), ShouldNotBeEmpty)
|
|
// check/get by reference
|
|
resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/" + digest.String())
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/" + digest.String())
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
So(resp.Body(), ShouldNotBeEmpty)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestManifestImageIndex(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
dir := t.TempDir()
|
|
ctlr := makeController(conf, dir)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
rthdlr := api.NewRouteHandler(ctlr)
|
|
|
|
cfg, layers, manifest, err := test.GetImageComponents(2) //nolint:staticcheck
|
|
So(err, ShouldBeNil)
|
|
|
|
content := []byte("this is a blob1")
|
|
digest := godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
|
|
// check a non-existent manifest
|
|
resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody(content).Head(baseURL + "/v2/unknown/manifests/test:1.0")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
repoName := "index"
|
|
err = test.UploadImage(
|
|
test.Image{
|
|
Config: cfg,
|
|
Layers: layers,
|
|
Manifest: manifest,
|
|
}, baseURL, repoName, "test:1.0")
|
|
So(err, ShouldBeNil)
|
|
|
|
_, err = os.Stat(path.Join(dir, "index"))
|
|
So(err, ShouldBeNil)
|
|
|
|
content, err = json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
digest = godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
m1content := content
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody(content).Put(baseURL + "/v2/index/manifests/test:1.0")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
digestHdr := resp.Header().Get(constants.DistContentDigestKey)
|
|
So(digestHdr, ShouldNotBeEmpty)
|
|
So(digestHdr, ShouldEqual, digest.String())
|
|
|
|
// create another manifest but upload using its sha256 reference
|
|
|
|
// upload image config blob
|
|
resp, err = resty.R().Post(baseURL + "/v2/index/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
|
|
img := test.CreateRandomImage()
|
|
|
|
err = test.UploadImage(img, baseURL, repoName, img.DigestStr())
|
|
So(err, ShouldBeNil)
|
|
|
|
content = img.ManifestDescriptor.Data
|
|
digest = img.ManifestDescriptor.Digest
|
|
|
|
m2dgst := digest
|
|
m2size := len(content)
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody(content).Put(baseURL + fmt.Sprintf("/v2/index/manifests/%s", digest))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
digestHdr = resp.Header().Get(constants.DistContentDigestKey)
|
|
So(digestHdr, ShouldNotBeEmpty)
|
|
So(digestHdr, ShouldEqual, digest.String())
|
|
|
|
Convey("Image index", func() {
|
|
img := test.CreateRandomImage()
|
|
|
|
err = test.UploadImage(img, baseURL, repoName, img.DigestStr())
|
|
So(err, ShouldBeNil)
|
|
|
|
content := img.ManifestDescriptor.Data
|
|
digest = img.ManifestDescriptor.Digest
|
|
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody(content).Put(baseURL + fmt.Sprintf("/v2/index/manifests/%s", digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
digestHdr := resp.Header().Get(constants.DistContentDigestKey)
|
|
So(digestHdr, ShouldNotBeEmpty)
|
|
So(digestHdr, ShouldEqual, digest.String())
|
|
|
|
var index ispec.Index
|
|
index.SchemaVersion = 2
|
|
index.Manifests = []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: digest,
|
|
Size: int64(len(content)),
|
|
},
|
|
{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: m2dgst,
|
|
Size: int64(m2size),
|
|
},
|
|
}
|
|
|
|
content, err = json.Marshal(index)
|
|
So(err, ShouldBeNil)
|
|
digest = godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
index1dgst := digest
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
SetBody(content).Put(baseURL + "/v2/index/manifests/test:index1")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
digestHdr = resp.Header().Get(constants.DistContentDigestKey)
|
|
So(digestHdr, ShouldNotBeEmpty)
|
|
So(digestHdr, ShouldEqual, digest.String())
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
Get(baseURL + "/v2/index/manifests/test:index1")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
So(resp.Body(), ShouldNotBeEmpty)
|
|
So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
|
|
|
|
img = test.CreateRandomImage()
|
|
|
|
err = test.UploadImage(img, baseURL, repoName, img.DigestStr())
|
|
So(err, ShouldBeNil)
|
|
|
|
content = img.ManifestDescriptor.Data
|
|
digest = img.ManifestDescriptor.Digest
|
|
|
|
m4dgst := digest
|
|
m4size := len(content)
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody(content).Put(baseURL + fmt.Sprintf("/v2/index/manifests/%s", digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
digestHdr = resp.Header().Get(constants.DistContentDigestKey)
|
|
So(digestHdr, ShouldNotBeEmpty)
|
|
So(digestHdr, ShouldEqual, digest.String())
|
|
|
|
index.SchemaVersion = 2
|
|
index.Manifests = []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: digest,
|
|
Size: int64(len(content)),
|
|
},
|
|
{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: m2dgst,
|
|
Size: int64(m2size),
|
|
},
|
|
}
|
|
|
|
content, err = json.Marshal(index)
|
|
So(err, ShouldBeNil)
|
|
digest = godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
SetBody(content).Put(baseURL + "/v2/index/manifests/test:index2")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
digestHdr = resp.Header().Get(constants.DistContentDigestKey)
|
|
So(digestHdr, ShouldNotBeEmpty)
|
|
So(digestHdr, ShouldEqual, digest.String())
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
Get(baseURL + "/v2/index/manifests/test:index2")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
So(resp.Body(), ShouldNotBeEmpty)
|
|
So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
|
|
|
|
Convey("List tags", func() {
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
|
|
request = mux.SetURLVars(request, map[string]string{"name": "index"})
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.ListTags(response, request)
|
|
|
|
resp := response.Result()
|
|
defer resp.Body.Close()
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusOK)
|
|
|
|
var tags api.ImageTags
|
|
err = json.NewDecoder(resp.Body).Decode(&tags)
|
|
So(err, ShouldBeNil)
|
|
So(len(tags.Tags), ShouldEqual, 3)
|
|
So(tags.Tags, ShouldContain, "test:1.0")
|
|
So(tags.Tags, ShouldContain, "test:index1")
|
|
So(tags.Tags, ShouldContain, "test:index2")
|
|
})
|
|
|
|
Convey("Another index with same manifest", func() {
|
|
var index ispec.Index
|
|
index.SchemaVersion = 2
|
|
index.Manifests = []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: m4dgst,
|
|
Size: int64(m4size),
|
|
},
|
|
}
|
|
|
|
content, err = json.Marshal(index)
|
|
So(err, ShouldBeNil)
|
|
digest = godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
SetBody(content).Put(baseURL + "/v2/index/manifests/test:index3")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
digestHdr = resp.Header().Get(constants.DistContentDigestKey)
|
|
So(digestHdr, ShouldNotBeEmpty)
|
|
So(digestHdr, ShouldEqual, digest.String())
|
|
})
|
|
|
|
Convey("Another index using digest with same manifest", func() {
|
|
var index ispec.Index
|
|
index.SchemaVersion = 2
|
|
index.Manifests = []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: m4dgst,
|
|
Size: int64(m4size),
|
|
},
|
|
}
|
|
|
|
content, err = json.Marshal(index)
|
|
So(err, ShouldBeNil)
|
|
digest = godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
SetBody(content).Put(baseURL + fmt.Sprintf("/v2/index/manifests/%s", digest))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
digestHdr = resp.Header().Get(constants.DistContentDigestKey)
|
|
So(digestHdr, ShouldNotBeEmpty)
|
|
So(digestHdr, ShouldEqual, digest.String())
|
|
})
|
|
|
|
Convey("Deleting manifest contained by a multiarch image should not be allowed", func() {
|
|
resp, err = resty.R().Delete(baseURL + fmt.Sprintf("/v2/index/manifests/%s", m2dgst.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
|
|
})
|
|
|
|
Convey("Deleting an image index", func() {
|
|
// delete manifest by tag should pass
|
|
resp, err = resty.R().Delete(baseURL + "/v2/index/manifests/test:index3")
|
|
So(err, ShouldBeNil)
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
Get(baseURL + "/v2/index/manifests/test:index3")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
So(resp.Body(), ShouldNotBeEmpty)
|
|
resp, err = resty.R().Delete(baseURL + "/v2/index/manifests/test:index1")
|
|
So(err, ShouldBeNil)
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
Get(baseURL + "/v2/index/manifests/test:index1")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
So(resp.Body(), ShouldNotBeEmpty)
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
Get(baseURL + "/v2/index/manifests/test:index2")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
So(resp.Body(), ShouldNotBeEmpty)
|
|
So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
|
|
})
|
|
|
|
Convey("Deleting an image index by digest", func() {
|
|
// delete manifest by tag should pass
|
|
resp, err = resty.R().Delete(baseURL + "/v2/index/manifests/test:index3")
|
|
So(err, ShouldBeNil)
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
Get(baseURL + "/v2/index/manifests/test:index3")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
So(resp.Body(), ShouldNotBeEmpty)
|
|
resp, err = resty.R().Delete(baseURL + fmt.Sprintf("/v2/index/manifests/%s", index1dgst))
|
|
So(err, ShouldBeNil)
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
Get(baseURL + "/v2/index/manifests/test:index1")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
So(resp.Body(), ShouldNotBeEmpty)
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
Get(baseURL + "/v2/index/manifests/test:index2")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
So(resp.Body(), ShouldNotBeEmpty)
|
|
So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
|
|
})
|
|
|
|
Convey("Update an index tag with different manifest", func() {
|
|
img := test.CreateRandomImage()
|
|
|
|
err = test.UploadImage(img, baseURL, repoName, img.DigestStr())
|
|
So(err, ShouldBeNil)
|
|
|
|
content = img.ManifestDescriptor.Data
|
|
digest = img.ManifestDescriptor.Digest
|
|
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody(content).Put(baseURL + fmt.Sprintf("/v2/index/manifests/%s", digest))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
digestHdr = resp.Header().Get(constants.DistContentDigestKey)
|
|
So(digestHdr, ShouldNotBeEmpty)
|
|
So(digestHdr, ShouldEqual, digest.String())
|
|
|
|
index.SchemaVersion = 2
|
|
index.Manifests = []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: digest,
|
|
Size: int64(len(content)),
|
|
},
|
|
}
|
|
|
|
content, err = json.Marshal(index)
|
|
So(err, ShouldBeNil)
|
|
digest = godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
SetBody(content).Put(baseURL + "/v2/index/manifests/test:index1")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
digestHdr = resp.Header().Get(constants.DistContentDigestKey)
|
|
So(digestHdr, ShouldNotBeEmpty)
|
|
So(digestHdr, ShouldEqual, digest.String())
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
Get(baseURL + "/v2/index/manifests/test:index1")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
So(resp.Body(), ShouldNotBeEmpty)
|
|
So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
|
|
|
|
// delete manifest by tag should pass
|
|
resp, err = resty.R().Delete(baseURL + "/v2/index/manifests/test:index1")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
Get(baseURL + "/v2/index/manifests/test:index1")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
So(resp.Body(), ShouldNotBeEmpty)
|
|
})
|
|
|
|
Convey("Negative test cases", func() {
|
|
Convey("Delete index", func() {
|
|
err = os.Remove(path.Join(dir, "index", "blobs", index1dgst.Algorithm().String(), index1dgst.Encoded()))
|
|
So(err, ShouldBeNil)
|
|
resp, err = resty.R().Delete(baseURL + fmt.Sprintf("/v2/index/manifests/%s", index1dgst))
|
|
So(err, ShouldBeNil)
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
Get(baseURL + "/v2/index/manifests/test:index1")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
So(resp.Body(), ShouldNotBeEmpty)
|
|
})
|
|
|
|
Convey("Corrupt index", func() {
|
|
err = os.WriteFile(path.Join(dir, "index", "blobs", index1dgst.Algorithm().String(), index1dgst.Encoded()),
|
|
[]byte("deadbeef"), storageConstants.DefaultFilePerms)
|
|
So(err, ShouldBeNil)
|
|
resp, err = resty.R().Delete(baseURL + fmt.Sprintf("/v2/index/manifests/%s", index1dgst))
|
|
So(err, ShouldBeNil)
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
Get(baseURL + "/v2/index/manifests/test:index1")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
|
So(resp.Body(), ShouldBeEmpty)
|
|
})
|
|
|
|
Convey("Change media-type", func() {
|
|
// previously a manifest, try writing an image index
|
|
var index ispec.Index
|
|
index.SchemaVersion = 2
|
|
index.Manifests = []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageIndex,
|
|
Digest: m4dgst,
|
|
Size: int64(m4size),
|
|
},
|
|
}
|
|
|
|
content, err = json.Marshal(index)
|
|
So(err, ShouldBeNil)
|
|
digest = godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
SetBody(content).Put(baseURL + "/v2/index/manifests/test:1.0")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
|
|
// previously an image index, try writing a manifest
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody(m1content).Put(baseURL + "/v2/index/manifests/test:index1")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestManifestCollision(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
dir := t.TempDir()
|
|
ctlr := makeController(conf, dir)
|
|
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Repositories: config.Repositories{
|
|
AuthorizationAllRepos: config.PolicyGroup{
|
|
AnonymousPolicy: []string{
|
|
constants.ReadPermission,
|
|
constants.CreatePermission,
|
|
constants.DeletePermission,
|
|
constants.DetectManifestCollisionPermission,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
cfg, layers, manifest, err := test.GetImageComponents(2) //nolint:staticcheck
|
|
So(err, ShouldBeNil)
|
|
|
|
err = test.UploadImage(
|
|
test.Image{
|
|
Config: cfg,
|
|
Layers: layers,
|
|
Manifest: manifest,
|
|
}, baseURL, "index", "test:1.0")
|
|
So(err, ShouldBeNil)
|
|
|
|
_, err = os.Stat(path.Join(dir, "index"))
|
|
So(err, ShouldBeNil)
|
|
|
|
content := []byte("this is a blob1")
|
|
digest := godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
|
|
// check a non-existent manifest
|
|
resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
|
|
SetBody(content).Head(baseURL + "/v2/unknown/manifests/test:1.0")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
content, err = json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
digest = godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
|
|
err = test.UploadImage(
|
|
test.Image{
|
|
Config: cfg,
|
|
Layers: layers,
|
|
Manifest: manifest,
|
|
}, baseURL, "index", "test:2.0")
|
|
So(err, ShouldBeNil)
|
|
|
|
// Deletion should fail if using digest
|
|
resp, err = resty.R().Delete(baseURL + "/v2/index/manifests/" + digest.String())
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusConflict)
|
|
|
|
// remove detectManifestCollision action from ** (all repos)
|
|
repoPolicy := conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos]
|
|
repoPolicy.AnonymousPolicy = []string{"read", "delete"}
|
|
conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy
|
|
|
|
resp, err = resty.R().Delete(baseURL + "/v2/index/manifests/" + digest.String())
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
|
|
resp, err = resty.R().Get(baseURL + "/v2/index/manifests/test:1.0")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
resp, err = resty.R().Get(baseURL + "/v2/index/manifests/test:2.0")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
})
|
|
}
|
|
|
|
func TestPullRange(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
dir := t.TempDir()
|
|
ctlr := makeController(conf, dir)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
// create a blob/layer
|
|
resp, err := resty.R().Post(baseURL + "/v2/index/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc := test.Location(baseURL, resp)
|
|
So(loc, ShouldNotBeEmpty)
|
|
|
|
// since we are not specifying any prefix i.e provided in config while starting server,
|
|
// so it should store index1 to global root dir
|
|
_, err = os.Stat(path.Join(dir, "index"))
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().Get(loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
|
|
content := []byte("0123456789")
|
|
digest := godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
// monolithic blob upload: success
|
|
resp, err = resty.R().SetQueryParam("digest", digest.String()).
|
|
SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
blobLoc := resp.Header().Get("Location")
|
|
So(blobLoc, ShouldNotBeEmpty)
|
|
So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
|
|
So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty)
|
|
blobLoc = baseURL + blobLoc
|
|
|
|
Convey("Range is supported using 'bytes'", func() {
|
|
resp, err = resty.R().Head(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.Header().Get("Accept-Ranges"), ShouldEqual, "bytes")
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
})
|
|
|
|
Convey("Get a range of bytes", func() {
|
|
resp, err = resty.R().SetHeader("Range", "bytes=0-").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusPartialContent)
|
|
So(resp.Header().Get("Content-Length"), ShouldEqual, fmt.Sprintf("%d", len(content)))
|
|
So(resp.Body(), ShouldResemble, content)
|
|
|
|
resp, err = resty.R().SetHeader("Range", "bytes=0-100").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusPartialContent)
|
|
So(resp.Header().Get("Content-Length"), ShouldEqual, fmt.Sprintf("%d", len(content)))
|
|
So(resp.Body(), ShouldResemble, content)
|
|
|
|
resp, err = resty.R().SetHeader("Range", "bytes=0-10").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusPartialContent)
|
|
So(resp.Header().Get("Content-Length"), ShouldEqual, fmt.Sprintf("%d", len(content)))
|
|
So(resp.Body(), ShouldResemble, content)
|
|
|
|
resp, err = resty.R().SetHeader("Range", "bytes=0-0").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusPartialContent)
|
|
So(resp.Header().Get("Content-Length"), ShouldEqual, "1")
|
|
So(resp.Body(), ShouldResemble, content[0:1])
|
|
|
|
resp, err = resty.R().SetHeader("Range", "bytes=0-1").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusPartialContent)
|
|
So(resp.Header().Get("Content-Length"), ShouldEqual, "2")
|
|
So(resp.Body(), ShouldResemble, content[0:2])
|
|
|
|
resp, err = resty.R().SetHeader("Range", "bytes=2-3").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusPartialContent)
|
|
So(resp.Header().Get("Content-Length"), ShouldEqual, "2")
|
|
So(resp.Body(), ShouldResemble, content[2:4])
|
|
})
|
|
|
|
Convey("Negative cases", func() {
|
|
resp, err = resty.R().SetHeader("Range", "=0").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
|
|
|
|
resp, err = resty.R().SetHeader("Range", "=a").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
|
|
|
|
resp, err = resty.R().SetHeader("Range", "").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
|
|
|
|
resp, err = resty.R().SetHeader("Range", "=").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
|
|
|
|
resp, err = resty.R().SetHeader("Range", "byte=").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
|
|
|
|
resp, err = resty.R().SetHeader("Range", "bytes=").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
|
|
|
|
resp, err = resty.R().SetHeader("Range", "byte=-0").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
|
|
|
|
resp, err = resty.R().SetHeader("Range", "byte=0").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
|
|
|
|
resp, err = resty.R().SetHeader("Range", "octet=-0").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
|
|
|
|
resp, err = resty.R().SetHeader("Range", "bytes=-0").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
|
|
|
|
resp, err = resty.R().SetHeader("Range", "bytes=1-0").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
|
|
|
|
resp, err = resty.R().SetHeader("Range", "bytes=-1-0").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
|
|
|
|
resp, err = resty.R().SetHeader("Range", "bytes=-1--0").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
|
|
|
|
resp, err = resty.R().SetHeader("Range", "bytes=1--2").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
|
|
|
|
resp, err = resty.R().SetHeader("Range", "bytes=0-a").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
|
|
|
|
resp, err = resty.R().SetHeader("Range", "bytes=a-10").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
|
|
|
|
resp, err = resty.R().SetHeader("Range", "bytes=a-b").Get(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestInjectInterruptedImageManifest(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
dir := t.TempDir()
|
|
ctlr := makeController(conf, dir)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
rthdlr := api.NewRouteHandler(ctlr)
|
|
|
|
Convey("Upload a blob & a config blob; Create an image manifest", func() {
|
|
// create a blob/layer
|
|
resp, err := resty.R().Post(baseURL + "/v2/repotest/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc := test.Location(baseURL, resp)
|
|
So(loc, ShouldNotBeEmpty)
|
|
|
|
// since we are not specifying any prefix i.e provided in config while starting server,
|
|
// so it should store repotest to global root dir
|
|
_, err = os.Stat(path.Join(dir, "repotest"))
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().Get(loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
|
|
content := []byte("this is a dummy blob")
|
|
digest := godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
// monolithic blob upload: success
|
|
resp, err = resty.R().SetQueryParam("digest", digest.String()).
|
|
SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
blobLoc := resp.Header().Get("Location")
|
|
So(blobLoc, ShouldNotBeEmpty)
|
|
So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
|
|
So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty)
|
|
|
|
// upload image config blob
|
|
resp, err = resty.R().Post(baseURL + "/v2/repotest/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc = test.Location(baseURL, resp)
|
|
cblob, cdigest := test.GetRandomImageConfig()
|
|
|
|
resp, err = resty.R().
|
|
SetContentLength(true).
|
|
SetHeader("Content-Length", fmt.Sprintf("%d", len(cblob))).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetQueryParam("digest", cdigest.String()).
|
|
SetBody(cblob).
|
|
Put(loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
// create a manifest
|
|
manifest := ispec.Manifest{
|
|
Config: ispec.Descriptor{
|
|
MediaType: "application/vnd.oci.image.config.v1+json",
|
|
Digest: cdigest,
|
|
Size: int64(len(cblob)),
|
|
},
|
|
Layers: []ispec.Descriptor{
|
|
{
|
|
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
|
Digest: digest,
|
|
Size: int64(len(content)),
|
|
},
|
|
},
|
|
}
|
|
manifest.SchemaVersion = 2
|
|
content, err = json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
digest = godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
|
|
// Testing router path: @Router /v2/{name}/manifests/{reference} [put]
|
|
Convey("Uploading an image manifest blob (when injected simulates an interrupted image manifest upload)", func() {
|
|
injected := inject.InjectFailure(0)
|
|
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, bytes.NewReader(content))
|
|
request = mux.SetURLVars(request, map[string]string{"name": "repotest", "reference": "1.0"})
|
|
request.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.UpdateManifest(response, request)
|
|
|
|
resp := response.Result()
|
|
defer resp.Body.Close()
|
|
|
|
So(resp, ShouldNotBeNil)
|
|
|
|
if injected {
|
|
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
|
|
} else {
|
|
So(resp.StatusCode, ShouldEqual, http.StatusCreated)
|
|
}
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestInjectTooManyOpenFiles(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
dir := t.TempDir()
|
|
ctlr := makeController(conf, dir)
|
|
conf.Storage.RemoteCache = false
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
rthdlr := api.NewRouteHandler(ctlr)
|
|
|
|
// create a blob/layer
|
|
resp, err := resty.R().Post(baseURL + "/v2/repotest/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc := test.Location(baseURL, resp)
|
|
So(loc, ShouldNotBeEmpty)
|
|
|
|
// since we are not specifying any prefix i.e provided in config while starting server,
|
|
// so it should store repotest to global root dir
|
|
_, err = os.Stat(path.Join(dir, "repotest"))
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().Get(loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
|
|
content := []byte("this is a dummy blob")
|
|
digest := godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
|
|
// monolithic blob upload
|
|
injected := inject.InjectFailure(2)
|
|
if injected {
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, loc, bytes.NewReader(content))
|
|
tokens := strings.Split(loc, "/")
|
|
request = mux.SetURLVars(request, map[string]string{"name": "repotest", "session_id": tokens[len(tokens)-1]})
|
|
q := request.URL.Query()
|
|
q.Add("digest", digest.String())
|
|
request.URL.RawQuery = q.Encode()
|
|
request.Header.Set("Content-Type", "application/octet-stream")
|
|
request.Header.Set("Content-Length", fmt.Sprintf("%d", len(content)))
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.UpdateBlobUpload(response, request)
|
|
|
|
resp := response.Result()
|
|
defer resp.Body.Close()
|
|
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
|
|
} else {
|
|
resp, err = resty.R().SetQueryParam("digest", digest.String()).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetBody(content).Put(loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
blobLoc := resp.Header().Get("Location")
|
|
So(blobLoc, ShouldNotBeEmpty)
|
|
So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
|
|
So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty)
|
|
}
|
|
|
|
// upload image config blob
|
|
resp, err = resty.R().Post(baseURL + "/v2/repotest/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc = test.Location(baseURL, resp)
|
|
cblob, cdigest := test.GetRandomImageConfig()
|
|
|
|
resp, err = resty.R().
|
|
SetContentLength(true).
|
|
SetHeader("Content-Length", fmt.Sprintf("%d", len(cblob))).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetQueryParam("digest", cdigest.String()).
|
|
SetBody(cblob).
|
|
Put(loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
// create a manifest
|
|
manifest := ispec.Manifest{
|
|
Config: ispec.Descriptor{
|
|
MediaType: "application/vnd.oci.image.config.v1+json",
|
|
Digest: cdigest,
|
|
Size: int64(len(cblob)),
|
|
},
|
|
Layers: []ispec.Descriptor{
|
|
{
|
|
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
|
Digest: digest,
|
|
Size: int64(len(content)),
|
|
},
|
|
},
|
|
}
|
|
manifest.SchemaVersion = 2
|
|
content, err = json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
digest = godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
|
|
// Testing router path: @Router /v2/{name}/manifests/{reference} [put]
|
|
//nolint:lll // gofumpt conflicts with lll
|
|
Convey("Uploading an image manifest blob (when injected simulates that PutImageManifest failed due to 'too many open files' error)", func() {
|
|
injected := inject.InjectFailure(2)
|
|
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, bytes.NewReader(content))
|
|
request = mux.SetURLVars(request, map[string]string{"name": "repotest", "reference": "1.0"})
|
|
request.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.UpdateManifest(response, request)
|
|
|
|
resp := response.Result()
|
|
So(resp, ShouldNotBeNil)
|
|
defer resp.Body.Close()
|
|
|
|
if injected {
|
|
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
|
|
} else {
|
|
So(resp.StatusCode, ShouldEqual, http.StatusCreated)
|
|
}
|
|
})
|
|
Convey("when injected simulates a `too many open files` error inside PutImageManifest method of img store", func() {
|
|
injected := inject.InjectFailure(2)
|
|
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, bytes.NewReader(content))
|
|
request = mux.SetURLVars(request, map[string]string{"name": "repotest", "reference": "1.0"})
|
|
request.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.UpdateManifest(response, request)
|
|
|
|
resp := response.Result()
|
|
defer resp.Body.Close()
|
|
|
|
So(resp, ShouldNotBeNil)
|
|
|
|
if injected {
|
|
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
|
|
} else {
|
|
So(resp.StatusCode, ShouldEqual, http.StatusCreated)
|
|
}
|
|
})
|
|
Convey("code coverage: error inside PutImageManifest method of img store (unable to marshal JSON)", func() {
|
|
injected := inject.InjectFailure(1)
|
|
|
|
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, bytes.NewReader(content))
|
|
request = mux.SetURLVars(request, map[string]string{"name": "repotest", "reference": "1.0"})
|
|
request.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
|
response := httptest.NewRecorder()
|
|
|
|
rthdlr.UpdateManifest(response, request)
|
|
|
|
resp := response.Result()
|
|
defer resp.Body.Close()
|
|
|
|
So(resp, ShouldNotBeNil)
|
|
|
|
if injected {
|
|
So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
|
|
} else {
|
|
So(resp.StatusCode, ShouldEqual, http.StatusCreated)
|
|
}
|
|
})
|
|
|
|
Convey("when index.json is not in json format", func() {
|
|
resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
|
|
SetBody(content).Put(baseURL + "/v2/repotest/manifests/v1.0")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
digestHdr := resp.Header().Get(constants.DistContentDigestKey)
|
|
So(digestHdr, ShouldNotBeEmpty)
|
|
So(digestHdr, ShouldEqual, digest.String())
|
|
|
|
indexFile := path.Join(dir, "repotest", "index.json")
|
|
_, err = os.Stat(indexFile)
|
|
So(err, ShouldBeNil)
|
|
indexContent := []byte(`not a JSON content`)
|
|
err = os.WriteFile(indexFile, indexContent, 0o600)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
|
|
SetBody(content).Put(baseURL + "/v2/repotest/manifests/v1.1")
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestGCSignaturesAndUntaggedManifests(t *testing.T) {
|
|
Convey("Make controller", t, func() {
|
|
Convey("Garbage collect signatures without subject and manifests without tags", func(c C) {
|
|
repoName := "testrepo" //nolint:goconst
|
|
tag := "0.0.1"
|
|
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
dir := t.TempDir()
|
|
ctlr.Config.Storage.RootDirectory = dir
|
|
ctlr.Config.Storage.GC = true
|
|
ctlr.Config.Storage.GCDelay = 1 * time.Millisecond
|
|
ctlr.Config.Storage.UntaggedImageRetentionDelay = 1 * time.Millisecond
|
|
|
|
ctlr.Config.Storage.Dedupe = false
|
|
|
|
err := test.WriteImageToFileSystem(test.CreateDefaultImage(), repoName, tag,
|
|
test.GetDefaultStoreController(dir, ctlr.Log))
|
|
So(err, ShouldBeNil)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartServer()
|
|
cm.WaitServerToBeReady(port)
|
|
defer cm.StopServer()
|
|
|
|
resp, err := resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, tag))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
digest := godigest.FromBytes(resp.Body())
|
|
So(digest, ShouldNotBeEmpty)
|
|
|
|
cwd, err := os.Getwd()
|
|
So(err, ShouldBeNil)
|
|
defer func() { _ = os.Chdir(cwd) }()
|
|
tdir := t.TempDir()
|
|
_ = os.Chdir(tdir)
|
|
|
|
// generate a keypair
|
|
os.Setenv("COSIGN_PASSWORD", "")
|
|
err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil)
|
|
So(err, ShouldBeNil)
|
|
|
|
image := fmt.Sprintf("localhost:%s/%s@%s", port, repoName, digest.String())
|
|
|
|
annotations := []string{fmt.Sprintf("tag=%s", tag)}
|
|
|
|
// sign the image
|
|
err = sign.SignCmd(&options.RootOptions{Verbose: true, Timeout: 1 * time.Minute},
|
|
options.KeyOpts{KeyRef: path.Join(tdir, "cosign.key"), PassFunc: generate.GetPass},
|
|
options.SignOptions{
|
|
Registry: options.RegistryOptions{AllowInsecure: true},
|
|
AnnotationOptions: options.AnnotationOptions{Annotations: annotations},
|
|
Upload: true,
|
|
},
|
|
[]string{image})
|
|
|
|
So(err, ShouldBeNil)
|
|
|
|
test.NotationPathLock.Lock()
|
|
defer test.NotationPathLock.Unlock()
|
|
|
|
test.LoadNotationPath(tdir)
|
|
|
|
// generate a keypair
|
|
err = test.GenerateNotationCerts(tdir, "good")
|
|
So(err, ShouldBeNil)
|
|
|
|
// sign the image
|
|
err = test.SignWithNotation("good", image, tdir)
|
|
So(err, ShouldBeNil)
|
|
|
|
// get cosign signature manifest
|
|
cosignTag := strings.Replace(digest.String(), ":", "-", 1) + "." + remote.SignatureTagSuffix
|
|
|
|
resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, cosignTag))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// get notation signature manifest
|
|
resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
|
|
fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
var index ispec.Index
|
|
|
|
err = json.Unmarshal(resp.Body(), &index)
|
|
So(err, ShouldBeNil)
|
|
So(len(index.Manifests), ShouldEqual, 1)
|
|
|
|
Convey("Trigger gcNotationSignatures() error", func() {
|
|
var refs ispec.Index
|
|
err = json.Unmarshal(resp.Body(), &refs)
|
|
|
|
err := os.Chmod(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded()), 0o000)
|
|
So(err, ShouldBeNil)
|
|
|
|
// trigger gc
|
|
img := test.CreateRandomImage()
|
|
|
|
err = test.UploadImage(img, baseURL, repoName, img.DigestStr())
|
|
So(err, ShouldBeNil)
|
|
|
|
err = ctlr.StoreController.DefaultStore.RunGCRepo(repoName)
|
|
So(err, ShouldNotBeNil)
|
|
|
|
err = os.Chmod(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded()), 0o755)
|
|
So(err, ShouldBeNil)
|
|
|
|
content, err := os.ReadFile(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded()))
|
|
So(err, ShouldBeNil)
|
|
err = os.WriteFile(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded()), []byte("corrupt"), 0o600) //nolint:lll
|
|
So(err, ShouldBeNil)
|
|
|
|
err = test.UploadImage(img, baseURL, repoName, tag)
|
|
So(err, ShouldBeNil)
|
|
|
|
err = ctlr.StoreController.DefaultStore.RunGCRepo(repoName)
|
|
So(err, ShouldNotBeNil)
|
|
|
|
err = os.WriteFile(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded()), content, 0o600)
|
|
So(err, ShouldBeNil)
|
|
})
|
|
|
|
Convey("Overwrite original image, signatures should be garbage-collected", func() {
|
|
// push an image without tag
|
|
cfg, layers, manifest, err := test.GetImageComponents(2) //nolint:staticcheck
|
|
So(err, ShouldBeNil)
|
|
|
|
manifestBuf, err := json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
untaggedManifestDigest := godigest.FromBytes(manifestBuf)
|
|
|
|
err = test.UploadImage(
|
|
test.Image{
|
|
Config: cfg,
|
|
Layers: layers,
|
|
Manifest: manifest,
|
|
}, baseURL, repoName, untaggedManifestDigest.String())
|
|
So(err, ShouldBeNil)
|
|
|
|
// overwrite image so that signatures will get invalidated and gc'ed
|
|
cfg, layers, manifest, err = test.GetImageComponents(3) //nolint:staticcheck
|
|
So(err, ShouldBeNil)
|
|
|
|
err = test.UploadImage(
|
|
test.Image{
|
|
Config: cfg,
|
|
Layers: layers,
|
|
Manifest: manifest,
|
|
}, baseURL, repoName, tag)
|
|
So(err, ShouldBeNil)
|
|
|
|
manifestBuf, err = json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
newManifestDigest := godigest.FromBytes(manifestBuf)
|
|
|
|
err = ctlr.StoreController.DefaultStore.RunGCRepo(repoName)
|
|
So(err, ShouldBeNil)
|
|
|
|
// both signatures should be gc'ed
|
|
resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, cosignTag))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
|
|
resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
|
|
fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
err = json.Unmarshal(resp.Body(), &index)
|
|
So(err, ShouldBeNil)
|
|
So(len(index.Manifests), ShouldEqual, 0)
|
|
|
|
resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
|
|
fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, newManifestDigest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
err = json.Unmarshal(resp.Body(), &index)
|
|
So(err, ShouldBeNil)
|
|
So(len(index.Manifests), ShouldEqual, 0)
|
|
|
|
// untagged image should also be gc'ed
|
|
resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, untaggedManifestDigest))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
|
|
})
|
|
})
|
|
|
|
Convey("Do not gc manifests which are part of a multiarch image", func(c C) {
|
|
repoName := "testrepo" //nolint:goconst
|
|
tag := "0.0.1"
|
|
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
dir := t.TempDir()
|
|
ctlr.Config.Storage.RootDirectory = dir
|
|
ctlr.Config.Storage.GC = true
|
|
ctlr.Config.Storage.GCDelay = 1 * time.Second
|
|
ctlr.Config.Storage.UntaggedImageRetentionDelay = 1 * time.Second
|
|
|
|
err := test.WriteImageToFileSystem(test.CreateDefaultImage(), repoName, tag,
|
|
test.GetDefaultStoreController(dir, ctlr.Log))
|
|
So(err, ShouldBeNil)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
resp, err := resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, tag))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
digest := godigest.FromBytes(resp.Body())
|
|
So(digest, ShouldNotBeEmpty)
|
|
|
|
// push an image index and make sure manifests contained by it are not gc'ed
|
|
// create an image index on upstream
|
|
var index ispec.Index
|
|
index.SchemaVersion = 2
|
|
index.MediaType = ispec.MediaTypeImageIndex
|
|
|
|
// upload multiple manifests
|
|
for i := 0; i < 4; i++ {
|
|
config, layers, manifest, err := test.GetImageComponents(1000 + i) //nolint:staticcheck
|
|
So(err, ShouldBeNil)
|
|
|
|
manifestContent, err := json.Marshal(manifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
manifestDigest := godigest.FromBytes(manifestContent)
|
|
|
|
err = test.UploadImage(
|
|
test.Image{
|
|
Manifest: manifest,
|
|
Config: config,
|
|
Layers: layers,
|
|
}, baseURL, repoName, manifestDigest.String())
|
|
So(err, ShouldBeNil)
|
|
|
|
index.Manifests = append(index.Manifests, ispec.Descriptor{
|
|
Digest: manifestDigest,
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Size: int64(len(manifestContent)),
|
|
})
|
|
}
|
|
|
|
content, err := json.Marshal(index)
|
|
So(err, ShouldBeNil)
|
|
indexDigest := godigest.FromBytes(content)
|
|
So(indexDigest, ShouldNotBeNil)
|
|
|
|
time.Sleep(1 * time.Second)
|
|
// upload image index
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
SetBody(content).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/latest", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
err = ctlr.StoreController.DefaultStore.RunGCRepo(repoName)
|
|
So(err, ShouldBeNil)
|
|
|
|
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
|
|
Get(baseURL + fmt.Sprintf("/v2/%s/manifests/latest", repoName))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
So(resp.Body(), ShouldNotBeEmpty)
|
|
So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
|
|
|
|
// make sure manifests which are part of image index are not gc'ed
|
|
for _, manifest := range index.Manifests {
|
|
resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, manifest.Digest.String()))
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestPeriodicGC(t *testing.T) {
|
|
Convey("Periodic gc enabled for default store", t, func() {
|
|
repoName := "testrepo" //nolint:goconst
|
|
|
|
port := test.GetFreePort()
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
conf.Storage.RemoteCache = false
|
|
|
|
logFile, err := os.CreateTemp("", "zot-log*.txt")
|
|
So(err, ShouldBeNil)
|
|
conf.Log.Output = logFile.Name()
|
|
defer os.Remove(logFile.Name()) // clean up
|
|
|
|
ctlr := api.NewController(conf)
|
|
dir := t.TempDir()
|
|
ctlr.Config.Storage.RootDirectory = dir
|
|
ctlr.Config.Storage.Dedupe = false
|
|
ctlr.Config.Storage.GC = true
|
|
ctlr.Config.Storage.GCInterval = 1 * time.Hour
|
|
ctlr.Config.Storage.GCDelay = 1 * time.Second
|
|
|
|
err = test.WriteImageToFileSystem(test.CreateDefaultImage(), repoName, "0.0.1",
|
|
test.GetDefaultStoreController(dir, ctlr.Log))
|
|
So(err, ShouldBeNil)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
time.Sleep(5000 * time.Millisecond)
|
|
|
|
data, err := os.ReadFile(logFile.Name())
|
|
So(err, ShouldBeNil)
|
|
So(string(data), ShouldContainSubstring,
|
|
"\"GC\":true,\"Commit\":false,\"GCDelay\":1000000000,\"GCInterval\":3600000000000")
|
|
So(string(data), ShouldContainSubstring,
|
|
fmt.Sprintf("executing GC of orphaned blobs for %s", path.Join(ctlr.StoreController.DefaultStore.RootDir(), repoName))) //nolint:lll
|
|
So(string(data), ShouldContainSubstring,
|
|
fmt.Sprintf("GC successfully completed for %s", path.Join(ctlr.StoreController.DefaultStore.RootDir(), repoName))) //nolint:lll
|
|
})
|
|
|
|
Convey("Periodic GC enabled for substore", t, func() {
|
|
port := test.GetFreePort()
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
|
|
logFile, err := os.CreateTemp("", "zot-log*.txt")
|
|
So(err, ShouldBeNil)
|
|
conf.Log.Output = logFile.Name()
|
|
defer os.Remove(logFile.Name()) // clean up
|
|
|
|
dir := t.TempDir()
|
|
ctlr := makeController(conf, dir)
|
|
subDir := t.TempDir()
|
|
|
|
subPaths := make(map[string]config.StorageConfig)
|
|
|
|
subPaths["/a"] = config.StorageConfig{
|
|
RootDirectory: subDir, GC: true, GCDelay: 1 * time.Second,
|
|
UntaggedImageRetentionDelay: 1 * time.Second, GCInterval: 24 * time.Hour, RemoteCache: false, Dedupe: false,
|
|
} //nolint:lll // gofumpt conflicts with lll
|
|
ctlr.Config.Storage.Dedupe = false
|
|
ctlr.Config.Storage.SubPaths = subPaths
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
data, err := os.ReadFile(logFile.Name())
|
|
So(err, ShouldBeNil)
|
|
// periodic GC is enabled by default for default store with a default interval
|
|
So(string(data), ShouldContainSubstring,
|
|
"\"GCDelay\":3600000000000,\"GCInterval\":3600000000000,\"")
|
|
// periodic GC is enabled for sub store
|
|
So(string(data), ShouldContainSubstring,
|
|
fmt.Sprintf("\"SubPaths\":{\"/a\":{\"RootDirectory\":\"%s\",\"Dedupe\":false,\"RemoteCache\":false,\"GC\":true,\"Commit\":false,\"GCDelay\":1000000000,\"GCInterval\":86400000000000", subDir)) //nolint:lll // gofumpt conflicts with lll
|
|
})
|
|
|
|
Convey("Periodic gc error", t, func() {
|
|
repoName := "testrepo" //nolint:goconst
|
|
|
|
port := test.GetFreePort()
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
conf.Storage.RemoteCache = false
|
|
|
|
logFile, err := os.CreateTemp("", "zot-log*.txt")
|
|
So(err, ShouldBeNil)
|
|
conf.Log.Output = logFile.Name()
|
|
defer os.Remove(logFile.Name()) // clean up
|
|
|
|
ctlr := api.NewController(conf)
|
|
dir := t.TempDir()
|
|
ctlr.Config.Storage.RootDirectory = dir
|
|
ctlr.Config.Storage.Dedupe = false
|
|
|
|
ctlr.Config.Storage.GC = true
|
|
ctlr.Config.Storage.GCInterval = 1 * time.Hour
|
|
ctlr.Config.Storage.GCDelay = 1 * time.Second
|
|
|
|
err = test.WriteImageToFileSystem(test.CreateDefaultImage(), repoName, "0.0.1",
|
|
test.GetDefaultStoreController(dir, ctlr.Log))
|
|
So(err, ShouldBeNil)
|
|
|
|
So(os.Chmod(dir, 0o000), ShouldBeNil)
|
|
|
|
defer func() {
|
|
So(os.Chmod(dir, 0o755), ShouldBeNil)
|
|
}()
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
time.Sleep(5000 * time.Millisecond)
|
|
|
|
data, err := os.ReadFile(logFile.Name())
|
|
So(err, ShouldBeNil)
|
|
So(string(data), ShouldContainSubstring,
|
|
"\"GC\":true,\"Commit\":false,\"GCDelay\":1000000000,\"GCInterval\":3600000000000")
|
|
So(string(data), ShouldContainSubstring, "failure walking storage root-dir") //nolint:lll
|
|
})
|
|
}
|
|
|
|
func TestSearchRoutes(t *testing.T) {
|
|
Convey("Upload image for test", t, func(c C) {
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
conf.HTTP.Port = port
|
|
tempDir := t.TempDir()
|
|
|
|
ctlr := makeController(conf, tempDir)
|
|
cm := test.NewControllerManager(ctlr)
|
|
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
repoName := "testrepo" //nolint:goconst
|
|
inaccessibleRepo := "inaccessible"
|
|
|
|
cfg, layers, manifest, err := test.GetImageComponents(10000) //nolint:staticcheck
|
|
So(err, ShouldBeNil)
|
|
|
|
err = test.UploadImage(
|
|
test.Image{
|
|
Config: cfg,
|
|
Layers: layers,
|
|
Manifest: manifest,
|
|
}, baseURL, repoName, "latest")
|
|
|
|
So(err, ShouldBeNil)
|
|
|
|
// data for the inaccessible repo
|
|
cfg, layers, manifest, err = test.GetImageComponents(10000) //nolint:staticcheck
|
|
So(err, ShouldBeNil)
|
|
|
|
err = test.UploadImage(
|
|
test.Image{
|
|
Config: cfg,
|
|
Layers: layers,
|
|
Manifest: manifest,
|
|
}, baseURL, inaccessibleRepo, "latest")
|
|
|
|
So(err, ShouldBeNil)
|
|
|
|
Convey("GlobalSearch with authz enabled", func(c C) {
|
|
conf := config.New()
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
user1 := "test"
|
|
password1 := "test"
|
|
testString1 := getCredString(user1, password1)
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
|
|
defer os.Remove(htpasswdPath)
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
|
|
conf.HTTP.Port = port
|
|
|
|
defaultVal := true
|
|
|
|
searchConfig := &extconf.SearchConfig{
|
|
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
|
}
|
|
|
|
conf.Extensions = &extconf.ExtensionConfig{
|
|
Search: searchConfig,
|
|
}
|
|
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Repositories: config.Repositories{
|
|
repoName: config.PolicyGroup{
|
|
Policies: []config.Policy{
|
|
{
|
|
Users: []string{user1},
|
|
Actions: []string{"read", "create"},
|
|
},
|
|
},
|
|
DefaultPolicy: []string{},
|
|
},
|
|
inaccessibleRepo: config.PolicyGroup{
|
|
Policies: []config.Policy{
|
|
{
|
|
Users: []string{user1},
|
|
Actions: []string{"create"},
|
|
},
|
|
},
|
|
DefaultPolicy: []string{},
|
|
},
|
|
},
|
|
AdminPolicy: config.Policy{
|
|
Users: []string{},
|
|
Actions: []string{},
|
|
},
|
|
}
|
|
|
|
ctlr := makeController(conf, tempDir)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
cfg, layers, manifest, err := test.GetImageComponents(10000) //nolint:staticcheck
|
|
So(err, ShouldBeNil)
|
|
|
|
err = test.UploadImageWithBasicAuth(
|
|
test.Image{
|
|
Config: cfg,
|
|
Layers: layers,
|
|
Manifest: manifest,
|
|
}, baseURL, repoName, "latest",
|
|
user1, password1)
|
|
So(err, ShouldBeNil)
|
|
|
|
// data for the inaccessible repo
|
|
cfg, layers, manifest, err = test.GetImageComponents(10000) //nolint:staticcheck
|
|
So(err, ShouldBeNil)
|
|
|
|
err = test.UploadImageWithBasicAuth(
|
|
test.Image{
|
|
Config: cfg,
|
|
Layers: layers,
|
|
Manifest: manifest,
|
|
}, baseURL, inaccessibleRepo, "latest",
|
|
user1, password1)
|
|
So(err, ShouldBeNil)
|
|
|
|
query := `
|
|
{
|
|
GlobalSearch(query:"testrepo"){
|
|
Repos {
|
|
Name
|
|
NewestImage {
|
|
RepoName
|
|
Tag
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
resp, err := resty.R().SetBasicAuth(user1, password1).Get(baseURL + constants.FullSearchPrefix +
|
|
"?query=" + url.QueryEscape(query))
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 200)
|
|
So(string(resp.Body()), ShouldContainSubstring, repoName)
|
|
So(string(resp.Body()), ShouldNotContainSubstring, inaccessibleRepo)
|
|
|
|
resp, err = resty.R().Get(baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query))
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Repositories: config.Repositories{
|
|
repoName: config.PolicyGroup{
|
|
Policies: []config.Policy{
|
|
{
|
|
Users: []string{user1},
|
|
Actions: []string{},
|
|
},
|
|
},
|
|
DefaultPolicy: []string{},
|
|
},
|
|
inaccessibleRepo: config.PolicyGroup{
|
|
Policies: []config.Policy{
|
|
{
|
|
Users: []string{},
|
|
Actions: []string{},
|
|
},
|
|
},
|
|
DefaultPolicy: []string{},
|
|
},
|
|
},
|
|
AdminPolicy: config.Policy{
|
|
Users: []string{},
|
|
Actions: []string{},
|
|
},
|
|
}
|
|
|
|
// authenticated, but no access to resource
|
|
resp, err = resty.R().SetBasicAuth(user1, password1).Get(baseURL + constants.FullSearchPrefix +
|
|
"?query=" + url.QueryEscape(query))
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
So(string(resp.Body()), ShouldNotContainSubstring, repoName)
|
|
So(string(resp.Body()), ShouldNotContainSubstring, inaccessibleRepo)
|
|
})
|
|
|
|
Convey("Testing group permissions", func(c C) {
|
|
conf := config.New()
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
user1 := "test1"
|
|
password1 := "test1"
|
|
group1 := "testgroup3"
|
|
testString1 := getCredString(user1, password1)
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
|
|
defer os.Remove(htpasswdPath)
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
|
|
conf.HTTP.Port = port
|
|
|
|
defaultVal := true
|
|
|
|
searchConfig := &extconf.SearchConfig{
|
|
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
|
}
|
|
|
|
conf.Extensions = &extconf.ExtensionConfig{
|
|
Search: searchConfig,
|
|
}
|
|
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Groups: config.Groups{
|
|
group1: {
|
|
Users: []string{user1},
|
|
},
|
|
},
|
|
Repositories: config.Repositories{
|
|
repoName: config.PolicyGroup{
|
|
Policies: []config.Policy{
|
|
{
|
|
Groups: []string{group1},
|
|
Actions: []string{"read", "create"},
|
|
},
|
|
},
|
|
DefaultPolicy: []string{},
|
|
},
|
|
},
|
|
AdminPolicy: config.Policy{
|
|
Users: []string{},
|
|
Actions: []string{},
|
|
},
|
|
}
|
|
|
|
ctlr := makeController(conf, tempDir)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
img := test.CreateRandomImage()
|
|
|
|
err = test.UploadImageWithBasicAuth(img, baseURL, repoName, img.DigestStr(), user1, password1)
|
|
So(err, ShouldBeNil)
|
|
|
|
query := `
|
|
{
|
|
GlobalSearch(query:"testrepo"){
|
|
Repos {
|
|
Name
|
|
NewestImage {
|
|
RepoName
|
|
Tag
|
|
}
|
|
}
|
|
}
|
|
}`
|
|
resp, err := resty.R().SetBasicAuth(user1, password1).Get(baseURL + constants.FullSearchPrefix +
|
|
"?query=" + url.QueryEscape(query))
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 200)
|
|
})
|
|
|
|
Convey("Testing group permissions when the user is part of more groups with different permissions", func(c C) {
|
|
conf := config.New()
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
user1 := "test2"
|
|
password1 := "test2"
|
|
group1 := "testgroup1"
|
|
group2 := "secondtestgroup"
|
|
testString1 := getCredString(user1, password1)
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
|
|
defer os.Remove(htpasswdPath)
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
|
|
conf.HTTP.Port = port
|
|
|
|
defaultVal := true
|
|
|
|
searchConfig := &extconf.SearchConfig{
|
|
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
|
}
|
|
|
|
conf.Extensions = &extconf.ExtensionConfig{
|
|
Search: searchConfig,
|
|
}
|
|
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Groups: config.Groups{
|
|
group1: {
|
|
Users: []string{user1},
|
|
},
|
|
},
|
|
Repositories: config.Repositories{
|
|
repoName: config.PolicyGroup{
|
|
Policies: []config.Policy{
|
|
{
|
|
Groups: []string{group1},
|
|
Actions: []string{"delete"},
|
|
},
|
|
{
|
|
Groups: []string{group2},
|
|
Actions: []string{"read", "create"},
|
|
},
|
|
},
|
|
DefaultPolicy: []string{},
|
|
},
|
|
},
|
|
AdminPolicy: config.Policy{
|
|
Users: []string{},
|
|
Actions: []string{},
|
|
},
|
|
}
|
|
|
|
ctlr := makeController(conf, tempDir)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
img := test.CreateRandomImage()
|
|
|
|
err = test.UploadImageWithBasicAuth(img, baseURL, repoName, img.DigestStr(), user1, password1)
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("Testing group permissions when group has less permissions than user", func(c C) {
|
|
conf := config.New()
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
user1 := "test3"
|
|
password1 := "test3"
|
|
group1 := "testgroup"
|
|
testString1 := getCredString(user1, password1)
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
|
|
defer os.Remove(htpasswdPath)
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
|
|
conf.HTTP.Port = port
|
|
|
|
defaultVal := true
|
|
|
|
searchConfig := &extconf.SearchConfig{
|
|
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
|
}
|
|
|
|
conf.Extensions = &extconf.ExtensionConfig{
|
|
Search: searchConfig,
|
|
}
|
|
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Groups: config.Groups{
|
|
group1: {
|
|
Users: []string{user1},
|
|
},
|
|
},
|
|
Repositories: config.Repositories{
|
|
repoName: config.PolicyGroup{
|
|
Policies: []config.Policy{
|
|
{
|
|
Groups: []string{group1},
|
|
Actions: []string{"delete"},
|
|
},
|
|
{
|
|
Users: []string{user1},
|
|
Actions: []string{"read", "create", "delete"},
|
|
},
|
|
},
|
|
DefaultPolicy: []string{},
|
|
},
|
|
},
|
|
AdminPolicy: config.Policy{
|
|
Users: []string{},
|
|
Actions: []string{},
|
|
},
|
|
}
|
|
|
|
ctlr := makeController(conf, tempDir)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
img := test.CreateRandomImage()
|
|
|
|
err = test.UploadImageWithBasicAuth(img, baseURL, repoName, img.DigestStr(), user1, password1)
|
|
So(err, ShouldBeNil)
|
|
})
|
|
|
|
Convey("Testing group permissions when user has less permissions than group", func(c C) {
|
|
conf := config.New()
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
user1 := "test4"
|
|
password1 := "test4"
|
|
group1 := "testgroup1"
|
|
testString1 := getCredString(user1, password1)
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
|
|
defer os.Remove(htpasswdPath)
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
|
|
conf.HTTP.Port = port
|
|
|
|
defaultVal := true
|
|
|
|
searchConfig := &extconf.SearchConfig{
|
|
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
|
}
|
|
|
|
conf.Extensions = &extconf.ExtensionConfig{
|
|
Search: searchConfig,
|
|
}
|
|
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Groups: config.Groups{
|
|
group1: {
|
|
Users: []string{user1},
|
|
},
|
|
},
|
|
Repositories: config.Repositories{
|
|
repoName: config.PolicyGroup{
|
|
Policies: []config.Policy{
|
|
{
|
|
Groups: []string{group1},
|
|
Actions: []string{"read", "create", "delete"},
|
|
},
|
|
{
|
|
Users: []string{user1},
|
|
Actions: []string{"delete"},
|
|
},
|
|
},
|
|
DefaultPolicy: []string{},
|
|
},
|
|
},
|
|
AdminPolicy: config.Policy{
|
|
Users: []string{},
|
|
Actions: []string{},
|
|
},
|
|
}
|
|
|
|
ctlr := makeController(conf, tempDir)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
img := test.CreateRandomImage()
|
|
|
|
err = test.UploadImageWithBasicAuth(img, baseURL, repoName, img.DigestStr(), user1, password1)
|
|
So(err, ShouldBeNil)
|
|
})
|
|
|
|
Convey("Testing group permissions on admin policy", func(c C) {
|
|
conf := config.New()
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
user1 := "test5"
|
|
password1 := "test5"
|
|
group1 := "testgroup2"
|
|
testString1 := getCredString(user1, password1)
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
|
|
defer os.Remove(htpasswdPath)
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
|
|
conf.HTTP.Port = port
|
|
|
|
defaultVal := true
|
|
|
|
searchConfig := &extconf.SearchConfig{
|
|
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
|
}
|
|
|
|
conf.Extensions = &extconf.ExtensionConfig{
|
|
Search: searchConfig,
|
|
}
|
|
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Groups: config.Groups{
|
|
group1: {
|
|
Users: []string{user1},
|
|
},
|
|
},
|
|
Repositories: config.Repositories{},
|
|
AdminPolicy: config.Policy{
|
|
Groups: []string{group1},
|
|
Actions: []string{"read", "create"},
|
|
},
|
|
}
|
|
|
|
ctlr := makeController(conf, tempDir)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
img := test.CreateRandomImage()
|
|
|
|
err = test.UploadImageWithBasicAuth(img, baseURL, repoName, img.DigestStr(), user1, password1)
|
|
So(err, ShouldBeNil)
|
|
})
|
|
|
|
Convey("Testing group permissions on anonymous policy", func(c C) {
|
|
conf := config.New()
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
conf.HTTP.Port = port
|
|
|
|
defaultVal := true
|
|
group1 := group
|
|
user1 := username
|
|
password1 := passphrase
|
|
|
|
testString1 := getCredString(user1, password1)
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
|
|
defer os.Remove(htpasswdPath)
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
|
|
searchConfig := &extconf.SearchConfig{
|
|
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
|
}
|
|
|
|
conf.Extensions = &extconf.ExtensionConfig{
|
|
Search: searchConfig,
|
|
}
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
|
Groups: config.Groups{
|
|
group1: {
|
|
Users: []string{user1},
|
|
},
|
|
},
|
|
Repositories: config.Repositories{
|
|
repoName: config.PolicyGroup{
|
|
Policies: []config.Policy{
|
|
{
|
|
Groups: []string{group1},
|
|
Actions: []string{"read", "create", "delete"},
|
|
},
|
|
{
|
|
Users: []string{user1},
|
|
Actions: []string{"delete"},
|
|
},
|
|
},
|
|
DefaultPolicy: []string{},
|
|
AnonymousPolicy: []string{"read", "create"},
|
|
},
|
|
},
|
|
}
|
|
|
|
ctlr := makeController(conf, tempDir)
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
img := test.CreateRandomImage()
|
|
|
|
err = test.UploadImageWithBasicAuth(img, baseURL, repoName, img.DigestStr(), "", "")
|
|
So(err, ShouldBeNil)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestDistSpecExtensions(t *testing.T) {
|
|
Convey("start zot server with search, ui and trust extensions", t, func(c C) {
|
|
conf := config.New()
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
conf.HTTP.Port = port
|
|
|
|
defaultVal := true
|
|
|
|
conf.Extensions = &extconf.ExtensionConfig{}
|
|
conf.Extensions.Search = &extconf.SearchConfig{}
|
|
conf.Extensions.Search.Enable = &defaultVal
|
|
conf.Extensions.Search.CVE = nil
|
|
conf.Extensions.UI = &extconf.UIConfig{}
|
|
conf.Extensions.UI.Enable = &defaultVal
|
|
conf.Extensions.Trust = &extconf.ImageTrustConfig{}
|
|
conf.Extensions.Trust.Enable = &defaultVal
|
|
conf.Extensions.Trust.Cosign = defaultVal
|
|
conf.Extensions.Trust.Notation = defaultVal
|
|
|
|
logFile, err := os.CreateTemp("", "zot-log*.txt")
|
|
So(err, ShouldBeNil)
|
|
conf.Log.Output = logFile.Name()
|
|
defer os.Remove(logFile.Name()) // clean up
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
var extensionList distext.ExtensionList
|
|
|
|
resp, err := resty.R().Get(baseURL + constants.RoutePrefix + constants.ExtOciDiscoverPrefix)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 200)
|
|
err = json.Unmarshal(resp.Body(), &extensionList)
|
|
So(err, ShouldBeNil)
|
|
t.Log(extensionList.Extensions)
|
|
So(len(extensionList.Extensions), ShouldEqual, 1)
|
|
So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 5)
|
|
So(extensionList.Extensions[0].Name, ShouldEqual, constants.BaseExtension)
|
|
So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md")
|
|
So(extensionList.Extensions[0].Description, ShouldNotBeEmpty)
|
|
// Verify the endpoints below are enabled by search
|
|
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullSearchPrefix)
|
|
// Verify the endpoints below are enabled by trust
|
|
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullCosign)
|
|
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullNotation)
|
|
// Verify the endpint below are enabled by having both the UI and the Search enabled
|
|
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullMgmt)
|
|
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullUserPrefs)
|
|
})
|
|
|
|
Convey("start zot server with only the search extension enabled", t, func(c C) {
|
|
conf := config.New()
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
conf.HTTP.Port = port
|
|
|
|
defaultVal := true
|
|
|
|
conf.Extensions = &extconf.ExtensionConfig{}
|
|
conf.Extensions.Search = &extconf.SearchConfig{}
|
|
conf.Extensions.Search.Enable = &defaultVal
|
|
|
|
logFile, err := os.CreateTemp("", "zot-log*.txt")
|
|
So(err, ShouldBeNil)
|
|
conf.Log.Output = logFile.Name()
|
|
defer os.Remove(logFile.Name()) // clean up
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
var extensionList distext.ExtensionList
|
|
|
|
resp, err := resty.R().Get(baseURL + constants.RoutePrefix + constants.ExtOciDiscoverPrefix)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 200)
|
|
err = json.Unmarshal(resp.Body(), &extensionList)
|
|
So(err, ShouldBeNil)
|
|
t.Log(extensionList.Extensions)
|
|
So(len(extensionList.Extensions), ShouldEqual, 1)
|
|
So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 2)
|
|
So(extensionList.Extensions[0].Name, ShouldEqual, constants.BaseExtension)
|
|
So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md")
|
|
So(extensionList.Extensions[0].Description, ShouldNotBeEmpty)
|
|
// Verify the endpoints below are enabled by search
|
|
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullSearchPrefix)
|
|
So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullMgmt)
|
|
// Verify the endpoints below are not enabled since trust is not enabled
|
|
So(extensionList.Extensions[0].Endpoints, ShouldNotContain, constants.FullCosign)
|
|
So(extensionList.Extensions[0].Endpoints, ShouldNotContain, constants.FullNotation)
|
|
// Verify the endpoints below are not enabled since the UI is not enabled
|
|
So(extensionList.Extensions[0].Endpoints, ShouldNotContain, constants.FullUserPrefs)
|
|
})
|
|
|
|
Convey("start zot server with no enabled extensions", t, func(c C) {
|
|
conf := config.New()
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
conf.HTTP.Port = port
|
|
|
|
logFile, err := os.CreateTemp("", "zot-log*.txt")
|
|
So(err, ShouldBeNil)
|
|
conf.Log.Output = logFile.Name()
|
|
defer os.Remove(logFile.Name()) // clean up
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
var extensionList distext.ExtensionList
|
|
|
|
resp, err := resty.R().Get(baseURL + constants.RoutePrefix + constants.ExtOciDiscoverPrefix)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 200)
|
|
err = json.Unmarshal(resp.Body(), &extensionList)
|
|
So(err, ShouldBeNil)
|
|
t.Log(extensionList.Extensions)
|
|
// Verify all endpoints which are disabled (even signing urls depend on search being enabled)
|
|
So(len(extensionList.Extensions), ShouldEqual, 0)
|
|
})
|
|
|
|
Convey("start minimal zot server", t, func(c C) {
|
|
conf := config.New()
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
conf.HTTP.Port = port
|
|
|
|
logFile, err := os.CreateTemp("", "zot-log*.txt")
|
|
So(err, ShouldBeNil)
|
|
conf.Log.Output = logFile.Name()
|
|
defer os.Remove(logFile.Name()) // clean up
|
|
|
|
ctlr := makeController(conf, t.TempDir())
|
|
|
|
cm := test.NewControllerManager(ctlr)
|
|
cm.StartAndWait(port)
|
|
defer cm.StopServer()
|
|
|
|
var extensionList distext.ExtensionList
|
|
resp, err := resty.R().Get(baseURL + constants.RoutePrefix + constants.ExtOciDiscoverPrefix)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 200)
|
|
|
|
err = json.Unmarshal(resp.Body(), &extensionList)
|
|
So(err, ShouldBeNil)
|
|
So(len(extensionList.Extensions), ShouldEqual, 0)
|
|
})
|
|
}
|
|
|
|
func TestHTTPOptionsResponse(t *testing.T) {
|
|
Convey("Test http options response", t, func() {
|
|
conf := config.New()
|
|
port := test.GetFreePort()
|
|
conf.HTTP.Port = port
|
|
baseURL := test.GetBaseURL(port)
|
|
|
|
ctlr := api.NewController(conf)
|
|
|
|
firstDir := t.TempDir()
|
|
|
|
secondDir := t.TempDir()
|
|
defer os.RemoveAll(firstDir)
|
|
defer os.RemoveAll(secondDir)
|
|
|
|
ctlr.Config.Storage.RootDirectory = firstDir
|
|
subPaths := make(map[string]config.StorageConfig)
|
|
subPaths["/a"] = config.StorageConfig{
|
|
RootDirectory: secondDir,
|
|
}
|
|
|
|
ctlr.Config.Storage.SubPaths = subPaths
|
|
ctrlManager := test.NewControllerManager(ctlr)
|
|
|
|
ctrlManager.StartAndWait(port)
|
|
|
|
resp, _ := resty.R().Options(baseURL + constants.RoutePrefix + constants.ExtCatalogPrefix)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
|
|
|
|
defer ctrlManager.StopServer()
|
|
})
|
|
}
|
|
|
|
func TestGetGithubUserInfo(t *testing.T) {
|
|
Convey("github api calls works", t, func() {
|
|
mockedHTTPClient := mock.NewMockedHTTPClient(
|
|
mock.WithRequestMatch(
|
|
mock.GetUserEmails,
|
|
[]github.UserEmail{
|
|
{
|
|
Email: github.String("test@test"),
|
|
Primary: github.Bool(true),
|
|
},
|
|
},
|
|
),
|
|
mock.WithRequestMatch(
|
|
mock.GetUserOrgs,
|
|
[]github.Organization{
|
|
{
|
|
Login: github.String("testOrg"),
|
|
},
|
|
},
|
|
),
|
|
)
|
|
|
|
client := github.NewClient(mockedHTTPClient)
|
|
|
|
_, _, err := api.GetGithubUserInfo(context.Background(), client, log.Logger{})
|
|
So(err, ShouldBeNil)
|
|
})
|
|
|
|
Convey("github ListEmails error", t, func() {
|
|
mockedHTTPClient := mock.NewMockedHTTPClient(
|
|
mock.WithRequestMatchHandler(
|
|
mock.GetUserEmails,
|
|
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
mock.WriteError(
|
|
w,
|
|
http.StatusInternalServerError,
|
|
"github error",
|
|
)
|
|
}),
|
|
),
|
|
)
|
|
|
|
client := github.NewClient(mockedHTTPClient)
|
|
|
|
_, _, err := api.GetGithubUserInfo(context.Background(), client, log.Logger{})
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
|
|
Convey("github ListEmails error", t, func() {
|
|
mockedHTTPClient := mock.NewMockedHTTPClient(
|
|
mock.WithRequestMatch(
|
|
mock.GetUserEmails,
|
|
[]github.UserEmail{
|
|
{
|
|
Email: github.String("test@test"),
|
|
Primary: github.Bool(true),
|
|
},
|
|
},
|
|
),
|
|
mock.WithRequestMatchHandler(
|
|
mock.GetUserOrgs,
|
|
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
mock.WriteError(
|
|
w,
|
|
http.StatusInternalServerError,
|
|
"github error",
|
|
)
|
|
}),
|
|
),
|
|
)
|
|
|
|
client := github.NewClient(mockedHTTPClient)
|
|
|
|
_, _, err := api.GetGithubUserInfo(context.Background(), client, log.Logger{})
|
|
So(err, ShouldNotBeNil)
|
|
})
|
|
}
|
|
|
|
func getAllBlobs(imagePath string) []string {
|
|
blobList := make([]string, 0)
|
|
|
|
if !common.DirExists(imagePath) {
|
|
return []string{}
|
|
}
|
|
|
|
buf, err := os.ReadFile(path.Join(imagePath, "index.json"))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
var index ispec.Index
|
|
if err := json.Unmarshal(buf, &index); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
var digest godigest.Digest
|
|
|
|
for _, m := range index.Manifests {
|
|
digest = m.Digest
|
|
blobList = append(blobList, digest.Encoded())
|
|
p := path.Join(imagePath, "blobs", digest.Algorithm().String(), digest.Encoded())
|
|
|
|
buf, err = os.ReadFile(p)
|
|
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
var manifest ispec.Manifest
|
|
if err := json.Unmarshal(buf, &manifest); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
blobList = append(blobList, manifest.Config.Digest.Encoded())
|
|
|
|
for _, layer := range manifest.Layers {
|
|
blobList = append(blobList, layer.Digest.Encoded())
|
|
}
|
|
}
|
|
|
|
return blobList
|
|
}
|
|
|
|
func getAllManifests(imagePath string) []string {
|
|
manifestList := make([]string, 0)
|
|
|
|
if !common.DirExists(imagePath) {
|
|
return []string{}
|
|
}
|
|
|
|
buf, err := os.ReadFile(path.Join(imagePath, "index.json"))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
var index ispec.Index
|
|
if err := json.Unmarshal(buf, &index); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
var digest godigest.Digest
|
|
|
|
for _, m := range index.Manifests {
|
|
digest = m.Digest
|
|
manifestList = append(manifestList, digest.Encoded())
|
|
}
|
|
|
|
return manifestList
|
|
}
|
|
|
|
func makeController(conf *config.Config, dir string) *api.Controller {
|
|
ctlr := api.NewController(conf)
|
|
|
|
ctlr.Config.Storage.RootDirectory = dir
|
|
|
|
return ctlr
|
|
}
|
|
|
|
func RunAuthorizationWithMultiplePoliciesTests(t *testing.T, userClient *resty.Client, bobClient *resty.Client,
|
|
baseURL string, conf *config.Config,
|
|
) {
|
|
t.Helper()
|
|
|
|
blob := []byte("hello, blob!")
|
|
digest := godigest.FromBytes(blob).String()
|
|
|
|
// unauthenticated clients should not have access to /v2/, no policy is applied since none exists
|
|
resp, err := resty.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 401)
|
|
|
|
repoPolicy := conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos]
|
|
repoPolicy.AnonymousPolicy = append(repoPolicy.AnonymousPolicy, "read")
|
|
conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy
|
|
|
|
// should have access to /v2/, anonymous policy is applied, "read" allowed
|
|
resp, err = resty.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// with empty username:password
|
|
resp, err = resty.R().SetHeader("Authorization", "Basic Og==").Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// add "test" user to global policy with create permission
|
|
repoPolicy.Policies[0].Users = append(repoPolicy.Policies[0].Users, "test")
|
|
repoPolicy.Policies[0].Actions = append(repoPolicy.Policies[0].Actions, "create")
|
|
|
|
// now it should get 202, user has the permission set on "create"
|
|
resp, err = userClient.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc := resp.Header().Get("Location")
|
|
|
|
// uploading blob should get 201
|
|
resp, err = userClient.R().
|
|
SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetQueryParam("digest", digest).
|
|
SetBody(blob).
|
|
Put(baseURL + loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
// head blob should get 403 without read perm
|
|
resp, err = userClient.R().Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
// get tags without read access should get 403
|
|
resp, err = userClient.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
repoPolicy.DefaultPolicy = append(repoPolicy.DefaultPolicy, "read")
|
|
conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy
|
|
|
|
// with read permission should get 200, because default policy allows reading now
|
|
resp, err = userClient.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// get tags with default read access should be ok, since the user is now "bob" and default policy is applied
|
|
resp, err = bobClient.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// get tags with anonymous read access should be ok
|
|
resp, err = resty.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// without create permission should get 403, since "bob" can only read(default policy applied)
|
|
resp, err = bobClient.R().
|
|
Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
// add read permission to user "bob"
|
|
conf.HTTP.AccessControl.AdminPolicy.Users = append(conf.HTTP.AccessControl.AdminPolicy.Users, "bob")
|
|
conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "create")
|
|
|
|
// added create permission to user "bob", should be allowed now
|
|
resp, err = bobClient.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
|
|
resp, err = resty.R().Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// make sure anonymous is correctly handled when using acCtx (requestcontext package)
|
|
catalog := struct {
|
|
Repositories []string `json:"repositories"`
|
|
}{}
|
|
|
|
err = json.Unmarshal(resp.Body(), &catalog)
|
|
So(err, ShouldBeNil)
|
|
So(catalog.Repositories, ShouldContain, AuthorizationNamespace)
|
|
|
|
resp, err = bobClient.R().Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
err = json.Unmarshal(resp.Body(), &catalog)
|
|
So(err, ShouldBeNil)
|
|
So(catalog.Repositories, ShouldContain, AuthorizationNamespace)
|
|
|
|
resp, err = userClient.R().Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
err = json.Unmarshal(resp.Body(), &catalog)
|
|
So(err, ShouldBeNil)
|
|
So(catalog.Repositories, ShouldContain, AuthorizationNamespace)
|
|
|
|
// no policy
|
|
conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = config.PolicyGroup{}
|
|
|
|
// no policies, so no anonymous allowed
|
|
resp, err = resty.R().Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
|
|
|
// bob is admin so he can read
|
|
resp, err = bobClient.R().Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
err = json.Unmarshal(resp.Body(), &catalog)
|
|
So(err, ShouldBeNil)
|
|
So(catalog.Repositories, ShouldContain, AuthorizationNamespace)
|
|
|
|
// test user has no permissions
|
|
resp, err = userClient.R().Get(baseURL + "/v2/_catalog")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
err = json.Unmarshal(resp.Body(), &catalog)
|
|
So(err, ShouldBeNil)
|
|
So(len(catalog.Repositories), ShouldEqual, 0)
|
|
}
|
|
|
|
func RunAuthorizationTests(t *testing.T, client *resty.Client, baseURL string, conf *config.Config) {
|
|
t.Helper()
|
|
|
|
Convey("run authorization tests", func() {
|
|
blob := []byte("hello, blob!")
|
|
digest := godigest.FromBytes(blob).String()
|
|
|
|
// unauthenticated clients should not have access to /v2/
|
|
resp, err := resty.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, 401)
|
|
|
|
// everybody should have access to /v2/
|
|
resp, err = client.R().Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// everybody should have access to /v2/_catalog
|
|
resp, err = client.R().Get(baseURL + constants.RoutePrefix + constants.ExtCatalogPrefix)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
var apiErr apiErr.Error
|
|
err = json.Unmarshal(resp.Body(), &apiErr)
|
|
So(err, ShouldBeNil)
|
|
|
|
// should get 403 without create
|
|
resp, err = client.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
// first let's use global based policies
|
|
// add test user to global policy with create perm
|
|
conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Users = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Users, "test") //nolint:lll // gofumpt conflicts with lll
|
|
|
|
conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll
|
|
|
|
// now it should get 202
|
|
resp, err = client.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc := resp.Header().Get("Location")
|
|
|
|
// uploading blob should get 201
|
|
resp, err = client.R().
|
|
SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetQueryParam("digest", digest).
|
|
SetBody(blob).
|
|
Put(baseURL + loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
// head blob should get 403 without read perm
|
|
resp, err = client.R().Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
// get tags without read access should get 403
|
|
resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
// get tags with read access should get 200
|
|
conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "read") //nolint:lll // gofumpt conflicts with lll
|
|
|
|
resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// head blob should get 200 now
|
|
resp, err = client.R().Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// get blob should get 200 now
|
|
resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// delete blob should get 403 without delete perm
|
|
resp, err = client.R().Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
// add delete perm on repo
|
|
conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "delete") //nolint:lll // gofumpt conflicts with lll
|
|
|
|
// delete blob should get 202
|
|
resp, err = client.R().Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
|
|
// now let's use only repository based policies
|
|
// add test user to repo's policy with create perm
|
|
// longest path matching should match the repo and not **/*
|
|
conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = config.PolicyGroup{
|
|
Policies: []config.Policy{
|
|
{
|
|
Users: []string{},
|
|
Actions: []string{},
|
|
},
|
|
},
|
|
DefaultPolicy: []string{},
|
|
}
|
|
|
|
conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users, "test") //nolint:lll // gofumpt conflicts with lll
|
|
conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll
|
|
|
|
// now it should get 202
|
|
resp, err = client.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc = resp.Header().Get("Location")
|
|
|
|
// uploading blob should get 201
|
|
resp, err = client.R().
|
|
SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetQueryParam("digest", digest).
|
|
SetBody(blob).
|
|
Put(baseURL + loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
// head blob should get 403 without read perm
|
|
resp, err = client.R().Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
// get tags without read access should get 403
|
|
resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
// get tags with read access should get 200
|
|
conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "read") //nolint:lll // gofumpt conflicts with lll
|
|
|
|
resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// head blob should get 200 now
|
|
resp, err = client.R().Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// get blob should get 200 now
|
|
resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// delete blob should get 403 without delete perm
|
|
resp, err = client.R().Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
// add delete perm on repo
|
|
conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "delete") //nolint:lll // gofumpt conflicts with lll
|
|
|
|
// delete blob should get 202
|
|
resp, err = client.R().Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
|
|
// remove permissions on **/* so it will not interfere with zot-test namespace
|
|
repoPolicy := conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos]
|
|
repoPolicy.Policies = []config.Policy{}
|
|
repoPolicy.DefaultPolicy = []string{}
|
|
conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy
|
|
|
|
// get manifest should get 403, we don't have perm at all on this repo
|
|
resp, err = client.R().Get(baseURL + "/v2/zot-test/manifests/0.0.1")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
// add read perm on repo
|
|
conf.HTTP.AccessControl.Repositories["zot-test"] = config.PolicyGroup{Policies: []config.Policy{
|
|
{
|
|
Users: []string{"test"},
|
|
Actions: []string{"read"},
|
|
},
|
|
}, DefaultPolicy: []string{}}
|
|
|
|
/* we have 4 images(authz/image, golang, zot-test, zot-cve-test) in storage,
|
|
but because at this point we only have read access
|
|
in authz/image and zot-test, we should get only that when listing repositories*/
|
|
resp, err = client.R().Get(baseURL + constants.RoutePrefix + constants.ExtCatalogPrefix)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
err = json.Unmarshal(resp.Body(), &apiErr)
|
|
So(err, ShouldBeNil)
|
|
|
|
catalog := struct {
|
|
Repositories []string `json:"repositories"`
|
|
}{}
|
|
|
|
err = json.Unmarshal(resp.Body(), &catalog)
|
|
So(err, ShouldBeNil)
|
|
So(len(catalog.Repositories), ShouldEqual, 2)
|
|
So(catalog.Repositories, ShouldContain, "zot-test")
|
|
So(catalog.Repositories, ShouldContain, AuthorizationNamespace)
|
|
|
|
// get manifest should get 200 now
|
|
resp, err = client.R().Get(baseURL + "/v2/zot-test/manifests/0.0.1")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
manifestBlob := resp.Body()
|
|
var manifest ispec.Manifest
|
|
|
|
err = json.Unmarshal(manifestBlob, &manifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
// put manifest should get 403 without create perm
|
|
resp, err = client.R().
|
|
SetBody(manifestBlob).
|
|
Put(baseURL + "/v2/zot-test/manifests/0.0.2")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
// add create perm on repo
|
|
conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll
|
|
|
|
// should get 201 with create perm
|
|
resp, err = client.R().
|
|
SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
|
|
SetBody(manifestBlob).
|
|
Put(baseURL + "/v2/zot-test/manifests/0.0.2")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
// create update config and post it.
|
|
cblob, cdigest := test.GetRandomImageConfig()
|
|
|
|
resp, err = client.R().
|
|
Post(baseURL + "/v2/zot-test/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc = test.Location(baseURL, resp)
|
|
|
|
// uploading blob should get 201
|
|
resp, err = client.R().
|
|
SetHeader("Content-Length", fmt.Sprintf("%d", len(cblob))).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetQueryParam("digest", cdigest.String()).
|
|
SetBody(cblob).
|
|
Put(loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
// create updated layer and post it
|
|
updateBlob := []byte("Hello, blob update!")
|
|
|
|
resp, err = client.R().Post(baseURL + "/v2/zot-test/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc = test.Location(baseURL, resp)
|
|
|
|
// uploading blob should get 201
|
|
resp, err = client.R().
|
|
SetHeader("Content-Length", fmt.Sprintf("%d", len(updateBlob))).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetQueryParam("digest", string(godigest.FromBytes(updateBlob))).
|
|
SetBody(updateBlob).
|
|
Put(loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
updatedManifest := ispec.Manifest{
|
|
Config: ispec.Descriptor{
|
|
MediaType: "application/vnd.oci.image.config.v1+json",
|
|
Digest: cdigest,
|
|
Size: int64(len(cblob)),
|
|
},
|
|
Layers: []ispec.Descriptor{
|
|
{
|
|
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
|
Digest: godigest.FromBytes(updateBlob),
|
|
Size: int64(len(updateBlob)),
|
|
},
|
|
},
|
|
}
|
|
updatedManifest.SchemaVersion = 2
|
|
updatedManifestBlob, err := json.Marshal(updatedManifest)
|
|
So(err, ShouldBeNil)
|
|
|
|
// update manifest should get 403 without update perm
|
|
resp, err = client.R().
|
|
SetBody(updatedManifestBlob).
|
|
Put(baseURL + "/v2/zot-test/manifests/0.0.2")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
// get the manifest and check if it's the old one
|
|
resp, err = client.R().Get(baseURL + "/v2/zot-test/manifests/0.0.2")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
So(resp.Body(), ShouldResemble, manifestBlob)
|
|
|
|
// add update perm on repo
|
|
conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions, "update") //nolint:lll // gofumpt conflicts with lll
|
|
|
|
// update manifest should get 201 with update perm
|
|
resp, err = client.R().
|
|
SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
|
|
SetBody(updatedManifestBlob).
|
|
Put(baseURL + "/v2/zot-test/manifests/0.0.2")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
// get the manifest and check if it's the new updated one
|
|
resp, err = client.R().Get(baseURL + "/v2/zot-test/manifests/0.0.2")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
So(resp.Body(), ShouldResemble, updatedManifestBlob)
|
|
|
|
// now use default repo policy
|
|
conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions = []string{}
|
|
repoPolicy = conf.HTTP.AccessControl.Repositories["zot-test"]
|
|
repoPolicy.DefaultPolicy = []string{"update"}
|
|
conf.HTTP.AccessControl.Repositories["zot-test"] = repoPolicy
|
|
|
|
// update manifest should get 201 with update perm on repo's default policy
|
|
resp, err = client.R().
|
|
SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
|
|
SetBody(manifestBlob).
|
|
Put(baseURL + "/v2/zot-test/manifests/0.0.2")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
// with default read on repo should still get 200
|
|
conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = []string{}
|
|
repoPolicy = conf.HTTP.AccessControl.Repositories[AuthorizationNamespace]
|
|
repoPolicy.DefaultPolicy = []string{"read"}
|
|
conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy
|
|
|
|
resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// upload blob without user create but with default create should get 200
|
|
repoPolicy.DefaultPolicy = append(repoPolicy.DefaultPolicy, "create")
|
|
conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy
|
|
|
|
resp, err = client.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
|
|
// remove per repo policy
|
|
repoPolicy = conf.HTTP.AccessControl.Repositories[AuthorizationNamespace]
|
|
repoPolicy.Policies = []config.Policy{}
|
|
repoPolicy.DefaultPolicy = []string{}
|
|
conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy
|
|
|
|
repoPolicy = conf.HTTP.AccessControl.Repositories["zot-test"]
|
|
repoPolicy.Policies = []config.Policy{}
|
|
repoPolicy.DefaultPolicy = []string{}
|
|
conf.HTTP.AccessControl.Repositories["zot-test"] = repoPolicy
|
|
|
|
resp, err = client.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
// whithout any perm should get 403
|
|
resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
// add read perm
|
|
conf.HTTP.AccessControl.AdminPolicy.Users = append(conf.HTTP.AccessControl.AdminPolicy.Users, "test")
|
|
conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "read")
|
|
|
|
// with read perm should get 200
|
|
resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
// without create perm should 403
|
|
resp, err = client.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
// add create perm
|
|
conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "create")
|
|
|
|
// with create perm should get 202
|
|
resp, err = client.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc = resp.Header().Get("Location")
|
|
|
|
// uploading blob should get 201
|
|
resp, err = client.R().
|
|
SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetQueryParam("digest", digest).
|
|
SetBody(blob).
|
|
Put(baseURL + loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
// without delete perm should 403
|
|
resp, err = client.R().Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
// add delete perm
|
|
conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "delete")
|
|
|
|
// with delete perm should get http.StatusAccepted
|
|
resp, err = client.R().Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
|
|
// without update perm should 403
|
|
resp, err = client.R().
|
|
SetBody(manifestBlob).
|
|
Put(baseURL + "/v2/zot-test/manifests/0.0.2")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
|
|
// add update perm
|
|
conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "update")
|
|
|
|
// update manifest should get 201 with update perm
|
|
resp, err = client.R().
|
|
SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
|
|
SetBody(manifestBlob).
|
|
Put(baseURL + "/v2/zot-test/manifests/0.0.2")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
|
|
conf.HTTP.AccessControl = &config.AccessControlConfig{}
|
|
|
|
resp, err = client.R().
|
|
SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
|
|
SetBody(manifestBlob).
|
|
Put(baseURL + "/v2/zot-test/manifests/0.0.2")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
|
})
|
|
}
|