0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -05:00
zot/pkg/api/controller_test.go
Andrei Aaron d42ac4cd0d
fix(delete manifest): distinct behaviors for delete by tag vb delete by digest (#2626)
In case of delete by tag only the tag is removed, the manifest itself would continue to be accessible by digest.
In case of delete by digest the manifest would be completely removed (provided it is not used by an index or another reference).

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2024-10-03 09:06:41 -07:00

12993 lines
405 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"
"sort"
"strconv"
"strings"
"testing"
"time"
"github.com/google/go-github/v62/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"
"github.com/opencontainers/image-spec/specs-go"
ispec "github.com/opencontainers/image-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"
"github.com/stretchr/testify/require"
"go.etcd.io/bbolt"
"gopkg.in/resty.v1"
"zotregistry.dev/zot/errors"
"zotregistry.dev/zot/pkg/api"
"zotregistry.dev/zot/pkg/api/config"
"zotregistry.dev/zot/pkg/api/constants"
apiErr "zotregistry.dev/zot/pkg/api/errors"
"zotregistry.dev/zot/pkg/cli/server"
"zotregistry.dev/zot/pkg/common"
extconf "zotregistry.dev/zot/pkg/extensions/config"
"zotregistry.dev/zot/pkg/log"
"zotregistry.dev/zot/pkg/meta"
"zotregistry.dev/zot/pkg/storage"
storageConstants "zotregistry.dev/zot/pkg/storage/constants"
"zotregistry.dev/zot/pkg/storage/gc"
storageTypes "zotregistry.dev/zot/pkg/storage/types"
authutils "zotregistry.dev/zot/pkg/test/auth"
test "zotregistry.dev/zot/pkg/test/common"
. "zotregistry.dev/zot/pkg/test/image-utils"
"zotregistry.dev/zot/pkg/test/inject"
"zotregistry.dev/zot/pkg/test/mocks"
ociutils "zotregistry.dev/zot/pkg/test/oci-utils"
"zotregistry.dev/zot/pkg/test/signature"
tskip "zotregistry.dev/zot/pkg/test/skip"
)
const (
ServerCert = "../../test/data/server.cert"
ServerKey = "../../test/data/server.key"
CACert = "../../test/data/ca.crt"
UnauthorizedNamespace = "fortknox/notallowed"
AuthorizationNamespace = "authz/image"
LDAPAddress = "127.0.0.1"
)
var (
username = "test" //nolint: gochecknoglobals
password = "test" //nolint: gochecknoglobals
group = "test" //nolint: gochecknoglobals
LDAPBaseDN = "ou=" + username //nolint: gochecknoglobals
LDAPBindDN = "cn=reader," + LDAPBaseDN //nolint: gochecknoglobals
LDAPBindPassword = "ldappass" //nolint: gochecknoglobals
)
func TestNew(t *testing.T) {
Convey("Make a new controller", t, func() {
conf := config.New()
So(conf, ShouldNotBeNil)
So(api.NewController(conf), ShouldNotBeNil)
})
Convey("Given a scale out cluster config where the local cluster socket cannot be found", t, func() {
conf := config.New()
So(conf, ShouldNotBeNil)
conf.HTTP = config.HTTPConfig{
Address: "127.0.0.2",
Port: "9000",
}
conf.Cluster = &config.ClusterConfig{
Members: []string{},
}
So(func() { api.NewController(conf) }, ShouldPanicWith, "failed to determine the local cluster socket")
})
Convey("Given a scale out cluster config where the local cluster socket cannot be found due to an error", t, func() {
conf := config.New()
So(conf, ShouldNotBeNil)
conf.HTTP = config.HTTPConfig{
Address: "127.0.0.2",
Port: "9000",
}
conf.Cluster = &config.ClusterConfig{
Members: []string{"127.0.0.1"},
}
So(func() { api.NewController(conf) }, ShouldPanicWith, "failed to get member socket")
})
}
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, err := storage.CreateCacheDatabaseDriver(conf.Storage.StorageConfig, log)
So(err, ShouldNotBeNil)
So(driver, ShouldBeNil)
conf.Storage.RemoteCache = true
conf.Storage.RootDirectory = t.TempDir()
driver, err = storage.CreateCacheDatabaseDriver(conf.Storage.StorageConfig, log)
So(err, ShouldBeNil)
So(driver, ShouldBeNil)
})
tskip.SkipDynamo(t)
tskip.SkipS3(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,
}
endpoint := os.Getenv("DYNAMODBMOCK_ENDPOINT")
conf.Storage.CacheDriver = map[string]interface{}{
"name": "dynamodb",
"endpoint": endpoint,
"region": "us-east-2",
"cachetablename": "BlobTable",
"repometatablename": "RepoMetadataTable",
"imagemetatablename": "ZotImageMetaTable",
"repoblobsinfotablename": "ZotRepoBlobsInfoTable",
"userdatatablename": "ZotUserDataTable",
"versiontablename": "Version",
}
driver, err := storage.CreateCacheDatabaseDriver(conf.Storage.StorageConfig, log)
So(err, ShouldBeNil)
So(driver, ShouldNotBeNil)
// negative test cases
conf.Storage.CacheDriver = map[string]interface{}{
"endpoint": endpoint,
"region": "us-east-2",
"cachetablename": "BlobTable",
"repometatablename": "RepoMetadataTable",
"imagemetatablename": "ZotImageMetaTable",
"repoblobsinfotablename": "ZotRepoBlobsInfoTable",
"userdatatablename": "ZotUserDataTable",
"versiontablename": "Version",
}
driver, err = storage.CreateCacheDatabaseDriver(conf.Storage.StorageConfig, log)
So(err, ShouldBeNil)
So(driver, ShouldBeNil)
conf.Storage.CacheDriver = map[string]interface{}{
"name": "dummy",
"endpoint": endpoint,
"region": "us-east-2",
"cachetablename": "BlobTable",
"repometatablename": "RepoMetadataTable",
"imagemetatablename": "ZotImageMetaTable",
"repoblobsinfotablename": "ZotRepoBlobsInfoTable",
"userdatatablename": "ZotUserDataTable",
"versiontablename": "Version",
}
driver, err = storage.CreateCacheDatabaseDriver(conf.Storage.StorageConfig, log)
So(err, ShouldBeNil)
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",
"imageMetaTablename": "ZotImageMetaTable",
"repoBlobsInfoTablename": "ZotRepoBlobsInfoTable",
"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",
"imageMetaTablename": "ZotImageMetaTable",
"repoBlobsInfoTablename": "ZotRepoBlobsInfoTable",
"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",
"imagemetatablename": "ZotImageMetaTable",
"repoblobsinfotablename": "ZotRepoBlobsInfoTable",
"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, "meta.db"), perms, &bbolt.Options{Timeout: time.Second * 10})
So(err, ShouldBeNil)
err = boltDB.Close()
So(err, ShouldBeNil)
err = os.Chmod(path.Join(dir, "meta.db"), 0o200)
So(err, ShouldBeNil)
_, err = meta.New(conf.Storage.StorageConfig, log)
So(err, ShouldNotBeNil)
err = os.Chmod(path.Join(dir, "meta.db"), 0o600)
So(err, ShouldBeNil)
defer os.Remove(path.Join(dir, "meta.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()
So(err, ShouldNotBeNil)
err = ctlr.Run()
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) {
tskip.SkipS3(t)
tskip.SkipDynamo(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
tmp := t.TempDir()
storageDriverParams := map[string]interface{}{
"rootdirectory": tmp,
"name": storageConstants.S3StorageDriverName,
}
conf.Storage.StorageDriver = storageDriverParams
ctlr := makeController(conf, tmp)
So(ctlr, ShouldNotBeNil)
err := ctlr.Init()
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")
tmp := t.TempDir()
storageDriverParams := map[string]interface{}{
"rootdirectory": tmp,
"name": storageConstants.S3StorageDriverName,
"region": "us-east-2",
"bucket": bucket,
"regionendpoint": endpoint,
"secure": false,
"skipverify": false,
}
conf.Storage.StorageDriver = storageDriverParams
ctlr := makeController(conf, tmp)
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": os.Getenv("DYNAMODBMOCK_ENDPOINT"),
"region": "us-east-2",
"cachetablename": "test",
"repometatablename": "RepoMetadataTable",
"imagemetatablename": "ZotImageMetaTable",
"repoblobsinfotablename": "ZotRepoBlobsInfoTable",
"userdatatablename": "ZotUserDataTable",
"apikeytablename": "APIKeyTable1",
"versiontablename": "Version",
}
mockOIDCServer, err := authutils.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) {
tskip.SkipS3(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")
tmp := t.TempDir()
storageDriverParams := map[string]interface{}{
"rootdirectory": tmp,
"name": storageConstants.S3StorageDriverName,
"region": "us-east-2",
"bucket": bucket,
"regionendpoint": endpoint,
"secure": false,
"skipverify": false,
}
conf.Storage.StorageDriver = storageDriverParams
ctlr := makeController(conf, tmp)
So(ctlr, ShouldNotBeNil)
subPathMap := make(map[string]config.StorageConfig)
subPathMap["/a"] = config.StorageConfig{
RootDirectory: t.TempDir(),
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, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
singleCredtests = append(singleCredtests, test.GetCredString(user, password))
singleCredtests = append(singleCredtests, test.GetCredString(user, password))
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())
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
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"
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(simpleUser, simpleUserPassword))
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,DELETE,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, test.GetCredString(user1, password1)+"\n"+
test.GetCredString(user2, password2))
twoCredTests = append(twoCredTests, test.GetCredString(user1, password1)+"\n"+
test.GetCredString(user2, password2)+"\n")
twoCredTests = append(twoCredTests, test.GetCredString(user1, password1)+"\n\n"+
test.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(test.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
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
defer os.Remove(htpasswdPath)
conf.HTTP.Auth = &config.AuthConfig{
HTPasswd: config.AuthHTPasswd{
Path: htpasswdPath,
},
}
ctlr := makeController(conf, t.TempDir())
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
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, password).Get(baseURL)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
resp, _ = resty.R().SetBasicAuth(username, password).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, seed := test.GenerateRandomName()
ctlr.Log.Info().Int64("seed", seed).Msg("random seed for repoName")
img := CreateRandomImage()
err = 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)
})
}
// tests for shared-storage scale-out cluster.
func TestScaleOutRequestProxy(t *testing.T) {
// when there is only one member, no proxying is expected and the responses should be correct.
Convey("Given a zot scale out cluster in http mode with only 1 member", t, func() {
port := test.GetFreePort()
clusterMembers := make([]string, 1)
clusterMembers[0] = "127.0.0.1:" + port
conf := config.New()
conf.HTTP.Port = port
conf.Cluster = &config.ClusterConfig{
Members: clusterMembers,
HashKey: "loremipsumdolors",
}
ctrlr := makeController(conf, t.TempDir())
cm := test.NewControllerManager(ctrlr)
cm.StartAndWait(port)
defer cm.StopServer()
Convey("Controller should start up and respond without error", func() {
resp, err := resty.R().Get(test.GetBaseURL(port) + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
})
Convey("Should upload images and fetch valid responses for repo tags list", func() {
reposToTest := []string{"debian", "alpine", "ubuntu"}
for _, repoName := range reposToTest {
img := CreateRandomImage()
err := UploadImage(img, test.GetBaseURL(port), repoName, "1.0")
So(err, ShouldBeNil)
resp, err := resty.R().Get(fmt.Sprintf("%s/v2/%s/tags/list", test.GetBaseURL(port), repoName))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
result := common.ImageTags{}
err = json.Unmarshal(resp.Body(), &result)
if err != nil {
t.Fatalf("Failed to unmarshal")
}
So(result.Name, ShouldEqual, repoName)
So(len(result.Tags), ShouldEqual, 1)
So(result.Tags[0], ShouldEqual, "1.0")
}
})
})
// when only one member in the cluster is online, an error is expected when there is a
// request proxied to an offline member.
Convey("Given a scale out http cluster with only 1 online member", t, func() {
port := test.GetFreePort()
clusterMembers := make([]string, 3)
clusterMembers[0] = "127.0.0.1:" + port
clusterMembers[1] = "127.0.0.1:1"
clusterMembers[2] = "127.0.0.1:2"
conf := config.New()
conf.HTTP.Port = port
conf.Cluster = &config.ClusterConfig{
Members: clusterMembers,
HashKey: "loremipsumdolors",
}
ctrlr := makeController(conf, t.TempDir())
cm := test.NewControllerManager(ctrlr)
cm.StartAndWait(port)
defer cm.StopServer()
Convey("Controller should start up and respond without error", func() {
resp, err := resty.R().Get(test.GetBaseURL(port) + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
})
Convey("Should fail to upload an image that is proxied to another instance", func() {
repoName := "alpine"
img := CreateRandomImage()
err := UploadImage(img, test.GetBaseURL(port), repoName, "1.0")
So(err, ShouldNotBeNil)
So(err.Error(), ShouldEqual, "can't post blob")
resp, err := resty.R().Get(fmt.Sprintf("%s/v2/%s/tags/list", test.GetBaseURL(port), repoName))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
})
})
// when there are multiple members in a cluster, requests are expected to return
// the same data for any member due to proxying.
Convey("Given a zot scale out cluster in http mode with 3 members", t, func() {
numMembers := 3
ports := make([]string, numMembers)
clusterMembers := make([]string, numMembers)
for idx := 0; idx < numMembers; idx++ {
port := test.GetFreePort()
ports[idx] = port
clusterMembers[idx] = "127.0.0.1:" + port
}
for _, port := range ports {
conf := config.New()
conf.HTTP.Port = port
conf.Cluster = &config.ClusterConfig{
Members: clusterMembers,
HashKey: "loremipsumdolors",
}
ctrlr := makeController(conf, t.TempDir())
cm := test.NewControllerManager(ctrlr)
cm.StartAndWait(port)
defer cm.StopServer()
}
Convey("All 3 controllers should start up and respond without error", func() {
for _, port := range ports {
resp, err := resty.R().Get(test.GetBaseURL(port) + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
}
})
Convey("Should upload images to repos and fetch same response from all 3 members", func() {
reposToTest := []string{"debian", "alpine", "ubuntu"}
for idx, repoName := range reposToTest {
img := CreateRandomImage()
// Upload to each instance based on loop counter
err := UploadImage(img, test.GetBaseURL(ports[idx]), repoName, "1.0")
So(err, ShouldBeNil)
// Query all 3 instances and expect the same response
for _, port := range ports {
resp, err := resty.R().Get(fmt.Sprintf("%s/v2/%s/tags/list", test.GetBaseURL(port), repoName))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
result := common.ImageTags{}
err = json.Unmarshal(resp.Body(), &result)
if err != nil {
t.Fatalf("Failed to unmarshal")
}
So(result.Name, ShouldEqual, repoName)
So(len(result.Tags), ShouldEqual, 1)
So(result.Tags[0], ShouldEqual, "1.0")
}
}
})
})
// this test checks for functionality when TLS and htpasswd auth are enabled.
// it primarily checks that headers are correctly copied over during the proxying process.
Convey("Given a zot scale out cluster in https mode with auth enabled", t, func() {
numMembers := 3
ports := make([]string, numMembers)
clusterMembers := make([]string, numMembers)
for idx := 0; idx < numMembers; idx++ {
port := test.GetFreePort()
ports[idx] = port
clusterMembers[idx] = "127.0.0.1:" + port
}
caCert, err := os.ReadFile(CACert)
So(err, ShouldBeNil)
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
username, _ := test.GenerateRandomString()
password, _ := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
defer os.Remove(htpasswdPath)
resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
defer func() { resty.SetTLSClientConfig(nil) }()
for _, port := range ports {
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,
},
}
conf.Cluster = &config.ClusterConfig{
Members: clusterMembers,
HashKey: "loremipsumdolors",
TLS: &config.TLSConfig{
CACert: CACert,
},
}
ctrlr := makeController(conf, t.TempDir())
cm := test.NewControllerManager(ctrlr)
cm.StartAndWait(port)
defer cm.StopServer()
}
Convey("All 3 controllers should start up and respond without error", func() {
for _, port := range ports {
resp, err := resty.R().SetBasicAuth(username, password).Get(test.GetSecureBaseURL(port) + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
}
})
Convey("Should upload images to repos and fetch same response from all 3 instances", func() {
reposToTest := []string{"debian", "alpine", "ubuntu"}
for idx, repoName := range reposToTest {
img := CreateRandomImage()
// Upload to each instance based on loop counter
err := UploadImageWithBasicAuth(img, test.GetSecureBaseURL(ports[idx]), repoName, "1.0", username, password)
So(err, ShouldBeNil)
// Query all 3 instances and expect the same response
for _, port := range ports {
resp, err := resty.R().SetBasicAuth(username, password).Get(
fmt.Sprintf("%s/v2/%s/tags/list", test.GetSecureBaseURL(port), repoName),
)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
result := common.ImageTags{}
err = json.Unmarshal(resp.Body(), &result)
if err != nil {
t.Fatalf("Failed to unmarshal")
}
So(result.Name, ShouldEqual, repoName)
So(len(result.Tags), ShouldEqual, 1)
So(result.Tags[0], ShouldEqual, "1.0")
}
}
})
})
// when the RootCA file does not exist, expect an error
Convey("Given a zot scale out cluster in with 2 members and an incorrect RootCACert", t, func() {
numMembers := 2
ports := make([]string, numMembers)
clusterMembers := make([]string, numMembers)
for idx := 0; idx < numMembers; idx++ {
port := test.GetFreePort()
ports[idx] = port
clusterMembers[idx] = "127.0.0.1:" + port
}
for _, port := range ports {
conf := config.New()
conf.HTTP.Port = port
conf.HTTP.TLS = &config.TLSConfig{
Cert: ServerCert,
Key: ServerKey,
}
conf.Cluster = &config.ClusterConfig{
Members: clusterMembers,
HashKey: "loremipsumdolors",
TLS: &config.TLSConfig{
CACert: "/tmp/does-not-exist.crt",
},
}
ctrlr := makeController(conf, t.TempDir())
cm := test.NewControllerManager(ctrlr)
cm.StartAndWait(port)
defer cm.StopServer()
}
caCert, err := os.ReadFile(CACert)
So(err, ShouldBeNil)
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
defer func() { resty.SetTLSClientConfig(nil) }()
Convey("Both controllers should start up and respond without error", func() {
for _, port := range ports {
resp, err := resty.R().Get(test.GetSecureBaseURL(port) + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
}
})
Convey("Proxying a request should fail with an error", func() {
// debian gets proxied to the second instance
resp, err := resty.R().Get(fmt.Sprintf("%s/v2/%s/tags/list", test.GetSecureBaseURL(ports[0]), "debian"))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
})
})
// when the server cert file does not exist, expect an error while proxying
Convey("Given a zot scale out cluster in with 2 members and an incorrect server cert", t, func() {
numMembers := 2
ports := make([]string, numMembers)
clusterMembers := make([]string, numMembers)
for idx := 0; idx < numMembers; idx++ {
port := test.GetFreePort()
ports[idx] = port
clusterMembers[idx] = "127.0.0.1:" + port
}
for _, port := range ports {
conf := config.New()
conf.HTTP.Port = port
conf.HTTP.TLS = &config.TLSConfig{
Cert: ServerCert,
Key: ServerKey,
}
conf.Cluster = &config.ClusterConfig{
Members: clusterMembers,
HashKey: "loremipsumdolors",
TLS: &config.TLSConfig{
CACert: CACert,
Cert: "/tmp/does-not-exist.crt",
},
}
ctrlr := makeController(conf, t.TempDir())
cm := test.NewControllerManager(ctrlr)
cm.StartAndWait(port)
defer cm.StopServer()
}
caCert, err := os.ReadFile(CACert)
So(err, ShouldBeNil)
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
defer func() { resty.SetTLSClientConfig(nil) }()
Convey("Both controllers should start up and respond without error", func() {
for _, port := range ports {
resp, err := resty.R().Get(test.GetSecureBaseURL(port) + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
}
})
Convey("Proxying a request should fail with an error", func() {
// debian gets proxied to the second instance
resp, err := resty.R().Get(fmt.Sprintf("%s/v2/%s/tags/list", test.GetSecureBaseURL(ports[0]), "debian"))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
})
})
// when the server key file does not exist, expect an error while proxying
Convey("Given a zot scale out cluster in with 2 members and an incorrect server key", t, func() {
numMembers := 2
ports := make([]string, numMembers)
clusterMembers := make([]string, numMembers)
for idx := 0; idx < numMembers; idx++ {
port := test.GetFreePort()
ports[idx] = port
clusterMembers[idx] = "127.0.0.1:" + port
}
for _, port := range ports {
conf := config.New()
conf.HTTP.Port = port
conf.HTTP.TLS = &config.TLSConfig{
Cert: ServerCert,
Key: ServerKey,
}
conf.Cluster = &config.ClusterConfig{
Members: clusterMembers,
HashKey: "loremipsumdolors",
TLS: &config.TLSConfig{
CACert: CACert,
Cert: ServerCert,
Key: "/tmp/does-not-exist.crt",
},
}
ctrlr := makeController(conf, t.TempDir())
cm := test.NewControllerManager(ctrlr)
cm.StartAndWait(port)
defer cm.StopServer()
}
caCert, err := os.ReadFile(CACert)
So(err, ShouldBeNil)
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
defer func() { resty.SetTLSClientConfig(nil) }()
Convey("Both controllers should start up and respond without error", func() {
for _, port := range ports {
resp, err := resty.R().Get(test.GetSecureBaseURL(port) + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
}
})
Convey("Proxying a request should fail with an error", func() {
// debian gets proxied to the second instance
resp, err := resty.R().Get(fmt.Sprintf("%s/v2/%s/tags/list", test.GetSecureBaseURL(ports[0]), "debian"))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
})
})
}
func TestPrintTracebackOnPanic(t *testing.T) {
Convey("Run server on unavailable port", t, func() {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
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
ctlr := makeController(conf, t.TempDir())
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
ctlr.StoreController.SubStore = make(map[string]storageTypes.ImageStore)
ctlr.StoreController.SubStore["/a"] = nil
resp, err := resty.R().Get(baseURL + "/v2/_catalog")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring, "invalid memory address or nil pointer dereference")
})
}
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() {
s1, seed1 := test.GenerateRandomName()
s2, seed2 := test.GenerateRandomName()
repoName := s1 + "/" + s2
ctlr.Log.Info().Int64("seed1", seed1).Int64("seed2", seed2).Msg("random seeds for repoName")
resp, err := client.R().Post(baseURL + "/v2/" + repoName + "/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", strconv.Itoa(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(repoName, 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/" + repoName + "/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() {
s1, seed1 := test.GenerateRandomName()
s2, seed2 := test.GenerateRandomName()
repoName := s1 + "/" + s2
ctlr.Log.Info().Int64("seed1", seed1).Int64("seed2", seed2).Msg("random seeds for repoName")
resp, err := client.R().Post(baseURL + "/v2/" + repoName + "/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", strconv.Itoa(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(repoName, sessionID)
if n > 0 && err == nil {
// cleaning blob uploads, so that zot fails to clean up, +code coverage
err = ctlr.StoreController.DefaultStore.DeleteBlobUpload(repoName, 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/" + repoName + "/blobs/uploads/" + sessionID)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
})
//nolint: dupl
Convey("Test interrupt PUT blob upload", func() {
s1, seed1 := test.GenerateRandomName()
s2, seed2 := test.GenerateRandomName()
repoName := s1 + "/" + s2
ctlr.Log.Info().Int64("seed1", seed1).Int64("seed2", seed2).Msg("random seeds for repoName")
resp, err := client.R().Post(baseURL + "/v2/" + repoName + "/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", strconv.Itoa(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(repoName, 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/" + repoName + "/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() {
s1, seed1 := test.GenerateRandomName()
s2, seed2 := test.GenerateRandomName()
repoName := s1 + "/" + s2
ctlr.Log.Info().Int64("seed1", seed1).Int64("seed2", seed2).Msg("random seeds for repoName")
resp, err := client.R().Post(baseURL + "/v2/" + repoName + "/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", strconv.Itoa(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(repoName, sessionID)
if n > 0 && err == nil {
// cleaning blob uploads, so that zot fails to clean up, +code coverage
err = ctlr.StoreController.DefaultStore.DeleteBlobUpload(repoName, 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/" + repoName + "/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
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
defer os.Remove(htpasswdPath)
conf.HTTP.Auth = &config.AuthConfig{
HTPasswd: config.AuthHTPasswd{
Path: htpasswdPath,
},
}
ctlr := api.NewController(conf)
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
err := ctlr.Init()
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, password).
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
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
defer os.Remove(htpasswdPath)
conf.HTTP.Auth = &config.AuthConfig{
HTPasswd: config.AuthHTPasswd{
Path: htpasswdPath,
},
}
globalDir := t.TempDir()
subDir := t.TempDir()
ctlr := makeController(conf, globalDir)
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
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, password).Get(baseURL)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
resp, _ = resty.R().SetBasicAuth(username, password).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()
conf := config.New()
conf.HTTP.Port = port
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
defer os.Remove(htpasswdPath)
conf.HTTP.Auth = &config.AuthConfig{
HTPasswd: config.AuthHTPasswd{
Path: htpasswdPath,
},
}
globalDir := t.TempDir()
subDir := t.TempDir()
ctlr := makeController(conf, globalDir)
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
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()
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()
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
err = ctlr.Init()
So(err, ShouldNotBeNil)
})
}
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)
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
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())
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
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, password).Get(secureBaseURL)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
resp, _ = resty.R().SetBasicAuth(username, password).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)
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
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{
test.AuthorizationAllRepos: config.PolicyGroup{
AnonymousPolicy: []string{"read"},
},
},
}
ctlr := makeController(conf, t.TempDir())
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
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, password).Get(secureBaseURL)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
resp, _ = resty.R().SetBasicAuth(username, password).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)
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{
test.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[test.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[test.AuthorizationAllRepos] = repoPolicy
resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
})
}
func TestAuthnErrors(t *testing.T) {
Convey("ldap CA certs fail", t, func() {
port := test.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
tmpDir := t.TempDir()
tmpFile := path.Join(tmpDir, "test-file.txt")
err := os.WriteFile(tmpFile, []byte("test"), 0o000)
So(err, ShouldBeNil)
conf.HTTP.Auth.LDAP = (&config.LDAPConfig{
Insecure: true,
Address: LDAPAddress,
Port: 9000,
BaseDN: LDAPBaseDN,
UserAttribute: "uid",
CACert: tmpFile,
}).SetBindDN(LDAPBindDN).SetBindPassword(LDAPBindPassword)
ctlr := makeController(conf, t.TempDir())
So(func() {
api.AuthHandler(ctlr)
}, ShouldPanic)
err = os.Chmod(tmpFile, 0o644)
So(err, ShouldBeNil)
})
Convey("ldap CA certs is empty", t, func() {
port := test.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
tmpDir := t.TempDir()
tmpFile := path.Join(tmpDir, "test-file.txt")
err := os.WriteFile(tmpFile, []byte(""), 0o600)
So(err, ShouldBeNil)
conf.HTTP.Auth.LDAP = (&config.LDAPConfig{
Insecure: true,
Address: LDAPAddress,
Port: 9000,
BaseDN: LDAPBaseDN,
UserAttribute: "uid",
CACert: tmpFile,
}).SetBindDN(LDAPBindDN).SetBindPassword(LDAPBindPassword)
ctlr := makeController(conf, t.TempDir())
So(func() {
api.AuthHandler(ctlr)
}, ShouldPanic)
So(err, ShouldBeNil)
})
Convey("ldap CA certs is empty", t, func() {
port := test.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
conf.HTTP.Auth.LDAP = (&config.LDAPConfig{
Insecure: true,
Address: LDAPAddress,
Port: 9000,
BaseDN: LDAPBaseDN,
UserAttribute: "uid",
CACert: CACert,
}).SetBindDN(LDAPBindDN).SetBindPassword(LDAPBindPassword)
ctlr := makeController(conf, t.TempDir())
So(func() {
api.AuthHandler(ctlr)
}, ShouldNotPanic)
})
Convey("Htpasswd file fail", t, func() {
port := test.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
tmpDir := t.TempDir()
tmpFile := path.Join(tmpDir, "test-file.txt")
err := os.WriteFile(tmpFile, []byte("test"), 0o000)
So(err, ShouldBeNil)
conf.HTTP.Auth.HTPasswd = config.AuthHTPasswd{
Path: tmpFile,
}
ctlr := makeController(conf, t.TempDir())
So(func() {
api.AuthHandler(ctlr)
}, ShouldPanic)
err = os.Chmod(tmpFile, 0o644)
So(err, ShouldBeNil)
})
Convey("NewRelyingPartyGithub fail", t, func() {
port := test.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
tmpDir := t.TempDir()
tmpFile := path.Join(tmpDir, "test-file.txt")
err := os.WriteFile(tmpFile, []byte("test"), 0o000)
So(err, ShouldBeNil)
conf.HTTP.Auth.HTPasswd = config.AuthHTPasswd{
Path: tmpFile,
}
So(func() {
api.NewRelyingPartyGithub(conf, "prov", nil, nil, log.NewLogger("debug", ""))
}, ShouldPanic)
err = os.Chmod(tmpFile, 0o644)
So(err, ShouldBeNil)
})
}
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{
test.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)
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
// with creds but without certs, should get conn error
_, err = resty.R().SetBasicAuth(username, password).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, password).Get(secureBaseURL)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
// with client certs, creds shouldn't matter
resp, _ = resty.R().SetBasicAuth(username, password).Get(secureBaseURL + "/v2/")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
})
}
func TestTSLFailedReadingOfCACert(t *testing.T) {
Convey("no permissions", t, func() {
port := test.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
conf.HTTP.TLS = &config.TLSConfig{
Cert: ServerCert,
Key: ServerKey,
CACert: CACert,
}
err := os.Chmod(CACert, 0o000)
defer func() {
err := os.Chmod(CACert, 0o644)
So(err, ShouldBeNil)
}()
So(err, ShouldBeNil)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctlr := makeController(conf, t.TempDir())
err = ctlr.Init()
So(err, ShouldBeNil)
errChan := make(chan error, 1)
go func() {
err = ctlr.Run()
errChan <- err
}()
testTimeout := false
select {
case err := <-errChan:
So(err, ShouldNotBeNil)
case <-ctx.Done():
testTimeout = true
cancel()
}
So(testTimeout, ShouldBeFalse)
})
Convey("empty CACert", t, func() {
badCACert := filepath.Join(t.TempDir(), "badCACert")
err := os.WriteFile(badCACert, []byte(""), 0o600)
So(err, ShouldBeNil)
port := test.GetFreePort()
conf := config.New()
conf.HTTP.Port = port
conf.HTTP.TLS = &config.TLSConfig{
Cert: ServerCert,
Key: ServerKey,
CACert: badCACert,
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctlr := makeController(conf, t.TempDir())
err = ctlr.Init()
So(err, ShouldBeNil)
errChan := make(chan error, 1)
go func() {
err = ctlr.Run()
errChan <- err
}()
testTimeout := false
select {
case err := <-errChan:
So(err, ShouldNotBeNil)
case <-ctx.Done():
testTimeout = true
cancel()
}
So(testTimeout, ShouldBeFalse)
})
}
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{
test.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)
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
// with creds but without certs, reads are allowed
resp, err = resty.R().SetBasicAuth(username, password).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, password).Get(secureBaseURL)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
// with client certs, creds shouldn't matter
resp, _ = resty.R().SetBasicAuth(username, password).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)
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
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())
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
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, password).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, password).Get(secureBaseURL)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
resp, _ = resty.R().SetBasicAuth(username, password).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)
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
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{
test.AuthorizationAllRepos: config.PolicyGroup{
AnonymousPolicy: []string{"read"},
},
},
}
ctlr := makeController(conf, t.TempDir())
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
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, password).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, password).Get(secureBaseURL)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
resp, _ = resty.R().SetBasicAuth(username, password).Get(secureBaseURL + "/v2/")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
})
}
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 bindSimplePw == "" {
switch bindDN {
case "bad-user", "cn=fail-user-bind,ou=test":
return vldap.LDAPResultInvalidCredentials, errors.ErrInvalidCred
default:
return vldap.LDAPResultSuccess, nil
}
}
if (bindDN == LDAPBindDN && bindSimplePw == LDAPBindPassword) ||
(bindDN == fmt.Sprintf("cn=%s,%s", username, LDAPBaseDN) && bindSimplePw == password) {
return vldap.LDAPResultSuccess, nil
}
return vldap.LDAPResultInvalidCredentials, errors.ErrInvalidCred
}
func (l *testLDAPServer) Search(boundDN string, req vldap.SearchRequest,
conn net.Conn,
) (vldap.ServerSearchResult, error) {
if req.Filter == "(uid=fail-user-bind)" {
return vldap.ServerSearchResult{
Entries: []*vldap.Entry{
{
DN: fmt.Sprintf("cn=%s,%s", "fail-user-bind", LDAPBaseDN),
Attributes: []*vldap.EntryAttribute{
{
Name: "memberOf",
Values: []string{group},
},
},
},
},
ResultCode: vldap.LDAPResultSuccess,
}, nil
}
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() {
ldapServer := newTestLDAPServer()
port := test.GetFreePort()
ldapPort, err := strconv.Atoi(port)
So(err, ShouldBeNil)
ldapServer.Start(ldapPort)
defer ldapServer.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,
BaseDN: LDAPBaseDN,
UserAttribute: "uid",
}).SetBindDN(LDAPBindDN).SetBindPassword(LDAPBindPassword),
}
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, password).Get(baseURL)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
resp, _ = resty.R().SetBasicAuth(username, password).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 TestBasicAuthWithReloadedCredentials(t *testing.T) {
Convey("Start server with bad credentials", t, func() {
l := newTestLDAPServer()
ldapPort, err := strconv.Atoi(test.GetFreePort())
So(err, ShouldBeNil)
l.Start(ldapPort)
defer l.Stop()
tempDir := t.TempDir()
ldapConfigContent := fmt.Sprintf(`{"BindDN": "%s", "BindPassword": "%s"}`, LDAPBindDN, LDAPBindPassword)
ldapConfigPath := filepath.Join(tempDir, "ldap.json")
err = os.WriteFile(ldapConfigPath, []byte(ldapConfigContent), 0o600)
So(err, ShouldBeNil)
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
configTemplate := `
{
"Storage": {
"rootDirectory": "%s"
},
"HTTP": {
"Address": "%s",
"Port": "%s",
"Auth": {
"LDAP": {
"CredentialsFile": "%s",
"UserAttribute": "uid",
"BaseDN": "LDAPBaseDN",
"UserGroupAttribute": "memberOf",
"Insecure": true,
"Address": "%v",
"Port": %v
}
}
}
}
`
configStr := fmt.Sprintf(configTemplate,
tempDir, "127.0.0.1", port, ldapConfigPath, LDAPAddress, ldapPort)
configPath := filepath.Join(tempDir, "config.json")
err = os.WriteFile(configPath, []byte(configStr), 0o600)
So(err, ShouldBeNil)
conf := config.New()
err = server.LoadConfiguration(conf, configPath)
So(err, ShouldBeNil)
ctlr := api.NewController(conf)
ctlrManager := test.NewControllerManager(ctlr)
hotReloader, err := server.NewHotReloader(ctlr, configPath, ldapConfigPath)
So(err, ShouldBeNil)
hotReloader.Start()
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
time.Sleep(time.Second * 2)
// test if the credentials work
resp, _ := resty.R().SetBasicAuth(username, password).Get(baseURL + "/v2/")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
newLdapConfigContent := `{"BindDN": "NewBindDB", "BindPassword": "NewBindPassword"}`
err = os.WriteFile(ldapConfigPath, []byte(newLdapConfigContent), 0o600)
So(err, ShouldBeNil)
for i := 0; i < 10; i++ {
// test if the credentials don't work
resp, _ = resty.R().SetBasicAuth(username, password).Get(baseURL + "/v2/")
So(resp, ShouldNotBeNil)
if resp.StatusCode() != http.StatusOK {
break
}
time.Sleep(100 * time.Millisecond)
}
So(resp.StatusCode(), ShouldNotEqual, http.StatusOK)
err = os.WriteFile(ldapConfigPath, []byte(ldapConfigContent), 0o600)
So(err, ShouldBeNil)
for i := 0; i < 10; i++ {
// test if the credentials don't work
resp, _ = resty.R().SetBasicAuth(username, password).Get(baseURL + "/v2/")
So(resp, ShouldNotBeNil)
if resp.StatusCode() == http.StatusOK {
break
}
time.Sleep(100 * time.Millisecond)
}
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// change the file
changedDir := t.TempDir()
changedLdapConfigPath := filepath.Join(changedDir, "ldap.json")
err = os.WriteFile(changedLdapConfigPath, []byte(newLdapConfigContent), 0o600)
So(err, ShouldBeNil)
configStr = fmt.Sprintf(configTemplate,
tempDir, "127.0.0.1", port, changedLdapConfigPath, LDAPAddress, ldapPort)
err = os.WriteFile(configPath, []byte(configStr), 0o600)
So(err, ShouldBeNil)
for i := 0; i < 10; i++ {
// test if the credentials don't work
resp, _ = resty.R().SetBasicAuth(username, password).Get(baseURL + "/v2/")
So(resp, ShouldNotBeNil)
if resp.StatusCode() != http.StatusOK {
break
}
time.Sleep(100 * time.Millisecond)
}
So(resp.StatusCode(), ShouldNotEqual, http.StatusOK)
err = os.WriteFile(changedLdapConfigPath, []byte(ldapConfigContent), 0o600)
So(err, ShouldBeNil)
for i := 0; i < 10; i++ {
// test if the credentials don't work
resp, _ = resty.R().SetBasicAuth(username, password).Get(baseURL + "/v2/")
So(resp, ShouldNotBeNil)
if resp.StatusCode() == http.StatusOK {
break
}
time.Sleep(100 * time.Millisecond)
}
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// make it panic
badLDAPFilePath := filepath.Join(changedDir, "ldap222.json")
err = os.WriteFile(badLDAPFilePath, []byte(newLdapConfigContent), 0o600)
So(err, ShouldBeNil)
configStr = fmt.Sprintf(configTemplate,
tempDir, "127.0.0.1", port, changedLdapConfigPath, LDAPAddress, ldapPort)
err = os.WriteFile(configPath, []byte(configStr), 0o600)
So(err, ShouldBeNil)
// Loading the config should fail because the file doesn't exist so the old credentials
// are still up and working fine.
for i := 0; i < 10; i++ {
// test if the credentials don't work
resp, _ = resty.R().SetBasicAuth(username, password).Get(baseURL + "/v2/")
So(resp, ShouldNotBeNil)
if resp.StatusCode() == http.StatusOK {
break
}
time.Sleep(100 * time.Millisecond)
}
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
})
}
func TestLDAPWithoutCreds(t *testing.T) {
Convey("Make a new LDAP server", t, func() {
ldapServer := newTestLDAPServer()
port := test.GetFreePort()
ldapPort, err := strconv.Atoi(port)
So(err, ShouldBeNil)
ldapServer.Start(ldapPort)
defer ldapServer.Stop()
Convey("Server credentials succed ldap auth", func() {
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,
BaseDN: LDAPBaseDN,
UserAttribute: "uid",
}).SetBindDN("anonym"),
}
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)
resp, _ = resty.R().SetBasicAuth(username, "").Get(baseURL)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
resp, _ = resty.R().SetBasicAuth(username, "").Get(baseURL + "/v2/")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
resp, _ = resty.R().SetBasicAuth(username, password).Get(baseURL + "/v2/")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
})
Convey("Server credentials fail ldap auth", func() {
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,
BaseDN: LDAPBaseDN,
UserAttribute: "uid",
}).SetBindDN("bad-user"),
}
ctlr := makeController(conf, t.TempDir())
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
resp, _ := resty.R().SetBasicAuth(username, password).Get(baseURL + "/v2/")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
})
})
}
func TestBasicAuthWithLDAPFromFile(t *testing.T) {
Convey("Make a new controller", t, func() {
ldapServer := newTestLDAPServer()
port := test.GetFreePort()
ldapPort, err := strconv.Atoi(port)
So(err, ShouldBeNil)
ldapServer.Start(ldapPort)
defer ldapServer.Stop()
port = test.GetFreePort()
baseURL := test.GetBaseURL(port)
tempDir := t.TempDir()
ldapConfigContent := fmt.Sprintf(`
{
"BindDN": "%v",
"BindPassword": "%v"
}`, LDAPBindDN, LDAPBindPassword)
ldapConfigPath := filepath.Join(tempDir, "ldap.json")
err = os.WriteFile(ldapConfigPath, []byte(ldapConfigContent), 0o600)
So(err, ShouldBeNil)
configStr := fmt.Sprintf(`
{
"Storage": {
"RootDirectory": "%s"
},
"HTTP": {
"Address": "%s",
"Port": "%s",
"Auth": {
"LDAP": {
"CredentialsFile": "%s",
"BaseDN": "%v",
"UserAttribute": "uid",
"UserGroupAttribute": "memberOf",
"Insecure": true,
"Address": "%v",
"Port": %v
}
}
}
}`, tempDir, "127.0.0.1", port, ldapConfigPath, LDAPBaseDN, LDAPAddress, ldapPort)
configPath := filepath.Join(tempDir, "config.json")
err = os.WriteFile(configPath, []byte(configStr), 0o0600)
So(err, ShouldBeNil)
server := server.NewServerRootCmd()
server.SetArgs([]string{"serve", configPath})
go func() {
err := server.Execute()
if err != nil {
panic(err)
}
}()
test.WaitTillServerReady(baseURL)
// 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, password).Get(baseURL)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
resp, _ = resty.R().SetBasicAuth(username, password).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 TestLDAPConfigErrors(t *testing.T) {
const configTemplate = `
{
"Storage": {
"RootDirectory": "%s"
},
"HTTP": {
"Address": "%s",
"Port": "%s",
"Auth": {
"LDAP": {
"CredentialsFile": "%s",
"BaseDN": "%v",
"UserAttribute": "%v",
"UserGroupAttribute": "memberOf",
"Insecure": true,
"Address": "%v",
"Port": %v
}
}
}
}`
Convey("bad credentials file", t, func() {
conf := config.New()
tempDir := t.TempDir()
ldapPort := 9000
userAttribute := ""
ldapConfigContent := `bad-json`
ldapConfigPath := filepath.Join(tempDir, "ldap.json")
err := os.WriteFile(ldapConfigPath, []byte(ldapConfigContent), 0o600)
So(err, ShouldBeNil)
configStr := fmt.Sprintf(configTemplate,
tempDir, "127.0.0.1", "8000", ldapConfigPath, LDAPBaseDN, userAttribute, LDAPAddress, ldapPort)
configPath := filepath.Join(tempDir, "config.json")
err = os.WriteFile(configPath, []byte(configStr), 0o0600)
So(err, ShouldBeNil)
err = server.LoadConfiguration(conf, configPath)
So(err, ShouldNotBeNil)
})
Convey("UserAttribute is empty", t, func() {
conf := config.New()
tempDir := t.TempDir()
ldapPort := 9000
userAttribute := ""
ldapConfigContent := fmt.Sprintf(`
{
"BindDN": "%v",
"BindPassword": "%v"
}`, LDAPBindDN, LDAPBindPassword)
ldapConfigPath := filepath.Join(tempDir, "ldap.json")
err := os.WriteFile(ldapConfigPath, []byte(ldapConfigContent), 0o600)
So(err, ShouldBeNil)
configStr := fmt.Sprintf(configTemplate,
tempDir, "127.0.0.1", "8000", ldapConfigPath, LDAPBaseDN, userAttribute, LDAPAddress, ldapPort)
configPath := filepath.Join(tempDir, "config.json")
err = os.WriteFile(configPath, []byte(configStr), 0o0600)
So(err, ShouldBeNil)
err = server.LoadConfiguration(conf, configPath)
So(err, ShouldNotBeNil)
})
Convey("address is empty", t, func() {
conf := config.New()
tempDir := t.TempDir()
ldapPort := 9000
userAttribute := "uid"
ldapConfigContent := fmt.Sprintf(`
{
"BindDN": "%v",
"BindPassword": "%v"
}`, LDAPBindDN, LDAPBindPassword)
ldapConfigPath := filepath.Join(tempDir, "ldap.json")
err := os.WriteFile(ldapConfigPath, []byte(ldapConfigContent), 0o600)
So(err, ShouldBeNil)
configStr := fmt.Sprintf(configTemplate,
tempDir, "127.0.0.1", "8000", ldapConfigPath, LDAPBaseDN, userAttribute, "", ldapPort)
configPath := filepath.Join(tempDir, "config.json")
err = os.WriteFile(configPath, []byte(configStr), 0o0600)
So(err, ShouldBeNil)
err = server.LoadConfiguration(conf, configPath)
So(err, ShouldNotBeNil)
})
Convey("BaseDN is empty", t, func() {
conf := config.New()
tempDir := t.TempDir()
ldapPort := 9000
userAttribute := "uid"
ldapConfigContent := fmt.Sprintf(`
{
"BindDN": "%v",
"BindPassword": "%v"
}`, LDAPBindDN, LDAPBindPassword)
ldapConfigPath := filepath.Join(tempDir, "ldap.json")
err := os.WriteFile(ldapConfigPath, []byte(ldapConfigContent), 0o600)
So(err, ShouldBeNil)
configStr := fmt.Sprintf(configTemplate,
tempDir, "127.0.0.1", "8000", ldapConfigPath, "", userAttribute, LDAPAddress, ldapPort)
configPath := filepath.Join(tempDir, "config.json")
err = os.WriteFile(configPath, []byte(configStr), 0o0600)
So(err, ShouldBeNil)
err = server.LoadConfiguration(conf, configPath)
So(err, ShouldNotBeNil)
})
}
func TestGroupsPermissionsForLDAP(t *testing.T) {
Convey("Make a new controller", t, func() {
ldapServer := newTestLDAPServer()
port := test.GetFreePort()
ldapPort, err := strconv.Atoi(port)
So(err, ShouldBeNil)
ldapServer.Start(ldapPort)
defer ldapServer.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,
BaseDN: LDAPBaseDN,
UserAttribute: "uid",
UserGroupAttribute: "memberOf",
}).SetBindDN(LDAPBindDN).SetBindPassword(LDAPBindPassword),
}
repoName, seed := test.GenerateRandomName()
conf.HTTP.AccessControl = &config.AccessControlConfig{
Groups: config.Groups{
group: {
Users: []string{username},
},
},
Repositories: config.Repositories{
repoName: 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)
ctlr.Log.Info().Int64("seed", seed).Msg("random seed for repoName")
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
img := CreateDefaultImage()
err = UploadImageWithBasicAuth(
img, baseURL, repoName, img.DigestStr(),
username, password)
So(err, ShouldBeNil)
})
}
func TestLDAPConfigFromFile(t *testing.T) {
Convey("Make a new controller", t, func() {
ldapServer := newTestLDAPServer()
port := test.GetFreePort()
ldapPort, err := strconv.Atoi(port)
So(err, ShouldBeNil)
ldapServer.Start(ldapPort)
defer ldapServer.Stop()
port = test.GetFreePort()
baseURL := test.GetBaseURL(port)
tempDir := t.TempDir()
ldapConfigContent := fmt.Sprintf(`
{
"bindDN": "%v",
"bindPassword": "%v"
}`, LDAPBindDN, LDAPBindPassword)
ldapConfigPath := filepath.Join(tempDir, "ldap.json")
err = os.WriteFile(ldapConfigPath, []byte(ldapConfigContent), 0o600)
So(err, ShouldBeNil)
configStr := fmt.Sprintf(`
{
"Storage": {
"RootDirectory": "%s"
},
"HTTP": {
"Address": "%s",
"Port": "%s",
"Auth": {
"LDAP": {
"CredentialsFile": "%s",
"BaseDN": "%v",
"UserAttribute": "uid",
"UserGroupAttribute": "memberOf",
"Insecure": true,
"Address": "%v",
"Port": %v
}
},
"AccessControl": {
"repositories": {
"test-ldap": {
"Policies": [
{
"Users": null,
"Actions": [
"read",
"create"
],
"Groups": [
"test"
]
}
]
}
},
"Groups": {
"test": {
"Users": [
"test"
]
}
}
}
}
}`, tempDir, "127.0.0.1", port, ldapConfigPath, LDAPBaseDN, LDAPAddress, ldapPort)
configPath := filepath.Join(tempDir, "config.json")
err = os.WriteFile(configPath, []byte(configStr), 0o0600)
So(err, ShouldBeNil)
server := server.NewServerRootCmd()
server.SetArgs([]string{"serve", configPath})
go func() {
err := server.Execute()
if err != nil {
panic(err)
}
}()
test.WaitTillServerReady(baseURL)
repo := "test-ldap"
img := CreateDefaultImage()
err = UploadImageWithBasicAuth(img, baseURL, repo, img.DigestStr(), username, password)
So(err, ShouldBeNil)
})
}
func TestLDAPFailures(t *testing.T) {
Convey("Make a LDAP conn", t, func() {
ldapServer := newTestLDAPServer()
port := test.GetFreePort()
ldapPort, err := strconv.Atoi(port)
So(err, ShouldBeNil)
ldapServer.Start(ldapPort)
defer ldapServer.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 TestLDAPClient(t *testing.T) {
Convey("LDAP Client", t, func() {
ldapServer := newTestLDAPServer()
port := test.GetFreePort()
ldapPort, err := strconv.Atoi(port)
So(err, ShouldBeNil)
ldapServer.Start(ldapPort)
defer ldapServer.Stop()
// bad server credentials
lClient := &api.LDAPClient{
Host: LDAPAddress,
Port: ldapPort,
BindDN: "bad-user",
BindPassword: "bad-pass",
SkipTLS: true,
}
_, _, _, err = lClient.Authenticate("bad-user", "bad-pass")
So(err, ShouldNotBeNil)
// bad credentials with anonymous authentication
lClient = &api.LDAPClient{
Host: LDAPAddress,
Port: ldapPort,
BindDN: "bad-user",
BindPassword: "",
SkipTLS: true,
}
_, _, _, err = lClient.Authenticate("user", "")
So(err, ShouldNotBeNil)
// bad user credentials with anonymous authentication
lClient = &api.LDAPClient{
Host: LDAPAddress,
Port: ldapPort,
BindDN: LDAPBindDN,
BindPassword: LDAPBindPassword,
Base: LDAPBaseDN,
UserFilter: "(uid=%s)",
SkipTLS: true,
}
_, _, _, err = lClient.Authenticate("fail-user-bind", "")
So(err, ShouldNotBeNil)
// bad user credentials with anonymous authentication
lClient = &api.LDAPClient{
Host: LDAPAddress,
Port: ldapPort,
BindDN: LDAPBindDN,
BindPassword: LDAPBindPassword,
Base: LDAPBaseDN,
UserFilter: "(uid=%s)",
SkipTLS: true,
}
_, _, _, err = lClient.Authenticate("fail-user-bind", "pass")
So(err, ShouldNotBeNil)
})
}
func TestBearerAuth(t *testing.T) {
Convey("Make a new controller", t, func() {
authTestServer := authutils.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 := authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
resp, err = resty.R().
SetQueryParam("service", authorizationHeader.Service).
Get(authorizationHeader.Realm)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
var goodToken authutils.AccessTokenResponse
err = json.Unmarshal(resp.Body(), &goodToken)
So(err, ShouldBeNil)
resp, err = resty.R().
SetHeader("Authorization", "Bearer "+goodToken.AccessToken).
Get(baseURL + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// trigger decode error
resp, err = resty.R().
SetHeader("Authorization", "Bearer "+"invalidToken").
Get(baseURL + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
resp, err = resty.R().SetHeader("Authorization",
"Bearer "+goodToken.AccessToken).Options(baseURL + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
s1, seed1 := test.GenerateRandomName()
s2, seed2 := test.GenerateRandomName()
repoName := s1 + "/" + s2
ctlr.Log.Info().Int64("seed1", seed1).Int64("seed2", seed2).Msg("random seeds for repoName")
resp, err = resty.R().Post(baseURL + "/v2/" + repoName + "/blobs/uploads/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
authorizationHeader = authutils.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", "Bearer "+goodToken.AccessToken).
Post(baseURL + "/v2/" + repoName + "/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", strconv.Itoa(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 = authutils.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", strconv.Itoa(len(blob))).
SetHeader("Content-Type", "application/octet-stream").
SetHeader("Authorization", "Bearer "+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", "Bearer "+goodToken.AccessToken).
Get(baseURL + "/v2/" + repoName + "/tags/list")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
authorizationHeader = authutils.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", "Bearer "+goodToken.AccessToken).
Get(baseURL + "/v2/" + repoName + "/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 = authutils.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 authutils.AccessTokenResponse
err = json.Unmarshal(resp.Body(), &badToken)
So(err, ShouldBeNil)
resp, err = resty.R().
SetHeader("Authorization", "Bearer "+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())
So(func() {
api.AuthHandler(ctlr)
}, ShouldPanic)
})
}
func TestBearerAuthWithAllowReadAccess(t *testing.T) {
Convey("Make a new controller", t, func() {
authTestServer := authutils.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{
test.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 := authutils.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 authutils.AccessTokenResponse
err = json.Unmarshal(resp.Body(), &goodToken)
So(err, ShouldBeNil)
resp, err = resty.R().
SetHeader("Authorization", "Bearer "+goodToken.AccessToken).
Get(baseURL + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
s1, seed1 := test.GenerateRandomName()
s2, seed2 := test.GenerateRandomName()
repoName := s1 + "/" + s2
ctlr.Log.Info().Int64("seed1", seed1).Int64("seed2", seed2).Msg("random seeds for repoName")
resp, err = resty.R().Post(baseURL + "/v2/" + repoName + "/blobs/uploads/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
authorizationHeader = authutils.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", "Bearer "+goodToken.AccessToken).
Post(baseURL + "/v2/" + repoName + "/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", strconv.Itoa(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 = authutils.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", strconv.Itoa(len(blob))).
SetHeader("Content-Type", "application/octet-stream").
SetHeader("Authorization", "Bearer "+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", "Bearer "+goodToken.AccessToken).
Get(baseURL + "/v2/" + repoName + "/tags/list")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
authorizationHeader = authutils.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", "Bearer "+goodToken.AccessToken).
Get(baseURL + "/v2/" + repoName + "/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 = authutils.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 authutils.AccessTokenResponse
err = json.Unmarshal(resp.Body(), &badToken)
So(err, ShouldBeNil)
resp, err = resty.R().
SetHeader("Authorization", "Bearer "+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()
ctx := context.Background()
mockOIDCServer, err := authutils.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(ctx, conf, "notDex", nil, nil, log.NewLogger("debug", "")) }, 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(ctx, conf, "oidc", nil, nil, log.NewLogger("debug", "")) }, ShouldPanic)
})
Convey("https callback", func() {
conf.HTTP.TLS = &config.TLSConfig{
Cert: ServerCert,
Key: ServerKey,
}
rp := api.NewRelyingPartyOIDC(ctx, conf, "oidc", nil, nil, log.NewLogger("debug", ""))
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(ctx, conf, "oidc", nil, nil, log.NewLogger("debug", ""))
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(ctx, conf, "oidc", nil, nil, log.NewLogger("debug", "")) }, 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
useSessionKeys bool
}{
{
address: "0.0.0.0",
externalURL: "http://" + 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",
},
{
address: "127.0.0.1",
externalURL: "",
testCaseName: "without ExternalURL provided in config and session keys for cookies",
useSessionKeys: true,
},
}
// need a username different than ldap one, to test both logic
htpasswdUsername, seedUser := test.GenerateRandomString()
htpasswdPassword, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(htpasswdUsername, htpasswdPassword))
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 := authutils.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,
BaseDN: LDAPBaseDN,
UserAttribute: "uid",
}).SetBindDN(LDAPBindDN).SetBindPassword(LDAPBindPassword),
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)
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
for _, testcase := range testCases {
t.Run(testcase.testCaseName, func(t *testing.T) {
if testcase.useSessionKeys {
ctlr.Config.HTTP.Auth.SessionHashKey = []byte("3lrioGLGO2RfG9Y7HQGgWa3ayBjMLw2auMXqEWcSXjQKc9SoQ3fKTIbO+toPYa7e")
ctlr.Config.HTTP.Auth.SessionEncryptKey = []byte("KOzt01JrDz2uC//UBC5ZikxQw4owfmI8")
}
Convey("make controller", t, func() {
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", 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)
})
//nolint: dupl
Convey("make sure sessions are not used without UI header value", func() {
sessionsNo, err := getNumberOfSessions(conf.Storage.RootDirectory)
So(err, ShouldBeNil)
So(sessionsNo, ShouldEqual, 0)
client := resty.New()
// without header should not create session
resp, err := client.R().SetBasicAuth(htpasswdUsername, htpasswdPassword).Get(baseURL + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
sessionsNo, err = getNumberOfSessions(conf.Storage.RootDirectory)
So(err, ShouldBeNil)
So(sessionsNo, ShouldEqual, 0)
client.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
resp, err = client.R().SetBasicAuth(htpasswdUsername, htpasswdPassword).Get(baseURL + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
sessionsNo, err = getNumberOfSessions(conf.Storage.RootDirectory)
So(err, ShouldBeNil)
So(sessionsNo, ShouldEqual, 1)
// set cookies
client.SetCookies(resp.Cookies())
// should get same cookie
resp, err = client.R().SetBasicAuth(htpasswdUsername, htpasswdPassword).Get(baseURL + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
sessionsNo, err = getNumberOfSessions(conf.Storage.RootDirectory)
So(err, ShouldBeNil)
So(sessionsNo, ShouldEqual, 1)
resp, err = client.R().SetBasicAuth(htpasswdUsername, htpasswdPassword).
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().
Get(baseURL + "/v2/_catalog")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = client.R().SetBasicAuth(htpasswdUsername, htpasswdPassword).Get(baseURL + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
sessionsNo, err = getNumberOfSessions(conf.Storage.RootDirectory)
So(err, ShouldBeNil)
So(sessionsNo, ShouldEqual, 1)
})
Convey("login with openid and get catalog with session", func() {
client := resty.New()
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
client.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
Convey("with callback_ui value provided", func() {
// first login user
resp, err := client.R().
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().
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().
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().
Options(baseURL + constants.LogoutPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
// logout user
resp, err = client.R().
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().
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()
client.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
// 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, htpasswdPassword).Get(baseURL)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = client.R().SetBasicAuth(htpasswdUsername, htpasswdPassword).Get(baseURL + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = client.R().SetBasicAuth(htpasswdUsername, htpasswdPassword).
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().
Get(baseURL + "/v2/_catalog")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = client.R().
Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// logout user
resp, err = client.R().
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().
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()
client.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
// 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, password).Get(baseURL)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = client.R().SetBasicAuth(username, password).Get(baseURL + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = client.R().SetBasicAuth(username, password).
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().
Get(baseURL + "/v2/_catalog")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = client.R().
Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// logout user
resp, err = client.R().
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().
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 := authutils.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
htpasswdUsername, seedUser := test.GenerateRandomString()
htpasswdPassword, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(htpasswdUsername, htpasswdPassword))
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 := authutils.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,
BaseDN: LDAPBaseDN,
UserAttribute: "uid",
}).SetBindDN(LDAPBindDN).SetBindPassword(LDAPBindPassword),
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.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
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, htpasswdPassword).
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, password).
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()
client.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
// first htpasswd saveSessionLoggedUser() error
resp, err := client.R().SetBasicAuth(htpasswdUsername, htpasswdPassword).
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, password).
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.Store.(*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["test.Username"] = username
cookieStore, ok := ctlr.CookieStore.Store.(*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
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
defer os.Remove(htpasswdPath)
mockOIDCServer, err := authutils.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.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
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, password).
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
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
defer os.Remove(htpasswdPath)
conf.HTTP.Auth = &config.AuthConfig{
HTPasswd: config.AuthHTPasswd{
Path: htpasswdPath,
},
}
conf.HTTP.AccessControl = &config.AccessControlConfig{
Repositories: config.Repositories{
test.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 := authutils.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.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
ctlr.Config.Storage.RootDirectory = t.TempDir()
err = WriteImageToFileSystem(CreateDefaultImage(), "zot-test", "0.0.1",
ociutils.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: username,
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, username, conf)
})
Convey("with basic auth", func() {
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
err := WriteImageToFileSystem(CreateDefaultImage(), "zot-test", "0.0.1",
ociutils.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, password)
RunAuthorizationTests(t, client, baseURL, username, conf)
})
})
}
func TestGetUsername(t *testing.T) {
Convey("Make a new controller", t, func() {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
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)
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
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, password).Get(baseURL + "/v2/_catalog")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
})
}
func TestAuthorizationMountBlob(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: one for user Policy, and another for default policy
username1, _ := test.GenerateRandomString()
password1, _ := test.GenerateRandomString()
username2, _ := test.GenerateRandomString()
password2, _ := test.GenerateRandomString()
username1 = strings.ToLower(username1)
username2 = strings.ToLower(username2)
content := test.GetCredString(username1, password1) + test.GetCredString(username2, password2)
htpasswdPath := test.MakeHtpasswdFileFromString(content)
defer os.Remove(htpasswdPath)
conf.HTTP.Auth = &config.AuthConfig{
HTPasswd: config.AuthHTPasswd{
Path: htpasswdPath,
},
}
user1Repo := username1 + "/**"
user2Repo := username2 + "/**"
// config with all policy types, to test that the correct one is applied in each case
conf.HTTP.AccessControl = &config.AccessControlConfig{
Repositories: config.Repositories{
user1Repo: config.PolicyGroup{
Policies: []config.Policy{
{
Users: []string{username1},
Actions: []string{
constants.ReadPermission,
constants.CreatePermission,
},
},
},
},
user2Repo: config.PolicyGroup{
Policies: []config.Policy{
{
Users: []string{username2},
Actions: []string{
constants.ReadPermission,
constants.CreatePermission,
},
},
},
},
},
}
dir := t.TempDir()
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = dir
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
userClient1 := resty.New()
userClient1.SetBasicAuth(username1, password1)
userClient2 := resty.New()
userClient2.SetBasicAuth(username2, password2)
img := CreateImageWith().RandomLayers(1, 2).DefaultConfig().Build()
repoName1 := username1 + "/" + "myrepo"
tag := "1.0"
// upload image with user1 on repoName1
err := UploadImageWithBasicAuth(img, baseURL, repoName1, tag, username1, password1)
So(err, ShouldBeNil)
repoName2 := username2 + "/" + "myrepo"
blobDigest := img.Manifest.Layers[0].Digest
/* a HEAD request by user2 on blob digest (found in user1Repo) should return 404
because user2 doesn't have permissions to read user1Repo */
resp, err := userClient2.R().Head(baseURL + fmt.Sprintf("/v2/%s/blobs/%s", repoName2, blobDigest))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
params := make(map[string]string)
params["mount"] = blobDigest.String()
// trying to mount a blob which can be found in cache, but user doesn't have permission
// should return 202 instead of 201
resp, err = userClient2.R().SetQueryParams(params).Post(baseURL + "/v2/" + repoName2 + "/blobs/uploads/")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
/* a HEAD request by user1 on blob digest (found in user1Repo) should return 200
because user1 has permission to read user1Repo */
resp, err = userClient1.R().Head(baseURL + fmt.Sprintf("/v2/%s/blobs/%s", username1+"/"+"mysecondrepo", blobDigest))
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// user2 can upload without dedupe
err = UploadImageWithBasicAuth(img, baseURL, repoName2, tag, username2, password2)
So(err, ShouldBeNil)
// trying to mount a blob which can be found in cache and user has permission should return 201 instead of 202
resp, err = userClient2.R().SetQueryParams(params).Post(baseURL + "/v2/" + repoName2 + "/blobs/uploads/")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
})
}
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", strconv.Itoa(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 := 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", strconv.Itoa(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", strconv.Itoa(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 = WriteImageToFileSystem(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"
htpasswdUsername, seedUser := test.GenerateRandomString()
htpasswdPassword, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(htpasswdUsername, htpasswdPassword))
defer os.Remove(htpasswdPath)
img := 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)
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
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, htpasswdPassword).
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, htpasswdPassword).
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 = UploadImage(img, baseURL, TestRepo, tagAnonymous)
So(err, ShouldNotBeNil)
err = UploadImageWithBasicAuth(img, baseURL,
TestRepo, tagAuth, htpasswdUsername, htpasswdPassword)
So(err, ShouldNotBeNil)
err = 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 = UploadImage(img, baseURL, TestRepo, tagAnonymous)
So(err, ShouldBeNil)
err = UploadImageWithBasicAuth(img, baseURL,
TestRepo, tagAuth, htpasswdUsername, htpasswdPassword)
So(err, ShouldBeNil)
err = 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, htpasswdPassword).
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: one for user Policy, and another for default policy
username1, seedUser1 := test.GenerateRandomString()
password1, seedPass1 := test.GenerateRandomString()
username2, seedUser2 := test.GenerateRandomString()
password2, seedPass2 := test.GenerateRandomString()
content := test.GetCredString(username1, password1) + test.GetCredString(username2, password2)
htpasswdPath := test.MakeHtpasswdFileFromString(content)
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{
test.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 := authutils.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.Log.Info().Int64("seedUser1", seedUser1).Int64("seedPass1", seedPass1).
Msg("random seed for username & password")
ctlr.Log.Info().Int64("seedUser2", seedUser2).Int64("seedPass2", seedPass2).
Msg("random seed for username & password")
ctlr.Config.Storage.RootDirectory = dir
err = WriteImageToFileSystem(CreateDefaultImage(), "zot-test", "0.0.1",
ociutils.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: username1,
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: username2,
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, username1, username2, conf)
})
Convey("with basic auth", func() {
dir := t.TempDir()
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = dir
err := WriteImageToFileSystem(CreateDefaultImage(), "zot-test", "0.0.1",
ociutils.GetDefaultStoreController(ctlr.Config.Storage.RootDirectory, ctlr.Log))
So(err, ShouldBeNil)
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
userClient1 := resty.New()
userClient1.SetBasicAuth(username1, password1)
userClient2 := resty.New()
userClient2.SetBasicAuth(username2, password2)
RunAuthorizationWithMultiplePoliciesTests(t, userClient1, userClient2, baseURL, username1, username2, 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
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
defer os.Remove(htpasswdPath)
conf.HTTP.Auth = &config.AuthConfig{
HTPasswd: config.AuthHTPasswd{
Path: htpasswdPath,
},
}
dir := t.TempDir()
ctlr := makeController(conf, dir)
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
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 := godigest.FromString("dummy").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, password).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, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
singleCredtests = append(singleCredtests, test.GetCredString(user, password))
singleCredtests = append(singleCredtests, test.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{
test.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())
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
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)
s1, seed1 := test.GenerateRandomName()
s2, seed2 := test.GenerateRandomName()
repoName := s1 + "/" + s2
ctlr.Log.Info().Int64("seed1", seed1).Int64("seed2", seed2).Msg("random seeds for repoName")
// with creds, any modifications should still fail on read-only mode
resp, err := resty.R().SetBasicAuth(user, password).
Post(baseURL + "/v2/" + repoName + "/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
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
defer os.Remove(htpasswdPath)
conf.HTTP.Auth = &config.AuthConfig{
HTPasswd: config.AuthHTPasswd{
Path: htpasswdPath,
},
}
dir := t.TempDir()
ctlr := api.NewController(conf)
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
ctlr.Config.Storage.RootDirectory = dir
ctlr.Config.Storage.RemoteCache = false
ctlr.Config.Storage.Dedupe = false
image := CreateDefaultImage()
err := WriteImageToFileSystem(image, "zot-cve-test", "test", storage.StoreController{
DefaultStore: ociutils.GetDefaultImageStore(dir, ctlr.Log),
})
So(err, ShouldBeNil)
cm := test.NewControllerManager(ctlr) //nolint: varnamelen
cm.StartAndWait(port)
params := make(map[string]string)
manifestDigest := image.ManifestDescriptor.Digest
dgst := manifestDigest
name := "zot-cve-test"
params["mount"] = string(manifestDigest)
params["from"] = name
client := resty.New()
headResponse, err := client.R().SetBasicAuth(username, password).
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, password).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"] = godigest.FromString("dummy").String()
incorrectParams["from"] = "zot-x-test"
postResponse, err = client.R().
SetBasicAuth(username, password).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, password).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, password).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, password).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, password).
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, password).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, password).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, password).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, password).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, password).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, password).
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, password).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, password).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(test.GetCredString(username, password))
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 := CreateImageWith().RandomLayers(1, 10).DefaultConfig().Build()
err := WriteImageToFileSystem(image, "zot-cve-test", "0.0.1",
ociutils.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, password).
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
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
t.Cleanup(func() {
os.Remove(htpasswdPath)
})
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.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
ctlr.Config.Storage.SubPaths = subPaths
testImagesDir := t.TempDir()
testImagesController := ociutils.GetDefaultStoreController(testImagesDir, ctlr.Log)
err := WriteImageToFileSystem(CreateRandomImage(), "zot-test", "0.0.1", testImagesController)
if err != nil {
t.Errorf("Error should be nil: %v", err)
}
err = WriteImageToFileSystem(CreateRandomImage(), "zot-cve-test", "0.0.1", testImagesController)
if err != nil {
t.Errorf("Error should be nil: %v", err)
}
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, password).
Get(baseURL + "/v2/" + testcase.destImageName + "/tags/list")
require.NoError(t, err, "Error should be nil")
assert.NotEqual(t, http.StatusBadRequest, tagResponse.StatusCode(), "bad request")
manifestList := getAllManifests(path.Join(testImagesDir, testcase.srcImageName))
for _, manifest := range manifestList {
headResponse, err := client.R().SetBasicAuth(username, password).
Head(baseURL + "/v2/" + testcase.destImageName + "/manifests/" + manifest)
require.NoError(t, err, "Error should be nil")
assert.Equal(t, http.StatusNotFound, headResponse.StatusCode(), "response status code should return 404")
getResponse, err := client.R().SetBasicAuth(username, password).
Get(baseURL + "/v2/" + testcase.destImageName + "/manifests/" + manifest)
require.NoError(t, err, "Error should be nil")
assert.Equal(t, http.StatusNotFound, getResponse.StatusCode(), "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, password).
Head(baseURL + "/v2/" + testcase.destImageName + "/blobs/sha256:" + blob)
require.NoError(t, err, "Error should be nil")
assert.NotEqual(t, http.StatusInternalServerError, headResponse.StatusCode(),
"internal server error should not occurred")
getResponse, err := client.R().SetBasicAuth(username, password).
Get(baseURL + "/v2/" + testcase.destImageName + "/blobs/sha256:" + blob)
require.NoError(t, err, "Error should be nil")
assert.NotEqual(t, http.StatusInternalServerError, getResponse.StatusCode(),
"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, password).
SetBody(buf).Post(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/")
require.NoError(t, err, "Error should be nil")
assert.NotEqual(t, http.StatusInternalServerError, postResponse.StatusCode(),
"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, password).
SetBody(buf).
Post(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/")
require.NoError(t, err, "Error should be nil")
assert.NotEqual(t, http.StatusInternalServerError, postResponse.StatusCode(),
"response status code should not return 500")
var sessionID string
sessionIDList := postResponse.Header().Values("Blob-Upload-UUID") //nolint:canonicalheader
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", strconv.Itoa(nbytes)).
SetHeader("Content-Range", strconv.Itoa(readContent)+"-"+strconv.Itoa(readContent+nbytes-1)).
SetBasicAuth(username, password).
Patch(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/" + sessionID)
require.NoError(t, err, "Error should be nil")
assert.NotEqual(t, http.StatusInternalServerError, patchResponse.StatusCode(),
"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, password).
Patch(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/" + sessionID)
if err != nil {
panic(err)
}
require.NoError(t, err, "Error should be nil")
assert.NotEqual(t, http.StatusInternalServerError, patchResponse.StatusCode(),
"response status code should not return 500")
}
}
} else {
postResponse, err = client.R().
SetHeader("Content-type", "application/octet-stream").
SetBasicAuth(username, password).
SetBody(buf).SetQueryParam("digest", "sha256:"+blob).
Post(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/")
require.NoError(t, err, "Error should be nil")
assert.NotEqual(t, http.StatusInternalServerError, postResponse.StatusCode(),
"response status code should not return 500")
}
headResponse, err = client.R().
SetBasicAuth(username, password).
Head(baseURL + "/v2/" + testcase.destImageName + "/blobs/sha256:" + blob)
require.NoError(t, err, "Error should be nil")
assert.NotEqual(t, http.StatusInternalServerError, headResponse.StatusCode(), "response should return success code")
getResponse, err = client.R().
SetBasicAuth(username, password).
Get(baseURL + "/v2/" + testcase.destImageName + "/blobs/sha256:" + blob)
require.NoError(t, err, "Error should be nil")
assert.NotEqual(t, http.StatusInternalServerError, getResponse.StatusCode(), "response should return success code")
}
tagResponse, err = client.R().SetBasicAuth(username, password).
Get(baseURL + "/v2/" + testcase.destImageName + "/tags/list")
require.NoError(t, err, "Error should be nil")
assert.Equal(t, http.StatusOK, tagResponse.StatusCode(), "response status code should return success code")
repoResponse, err := client.R().SetBasicAuth(username, password).
Get(baseURL + constants.RoutePrefix + constants.ExtCatalogPrefix)
require.NoError(t, err, "Error should be nil")
assert.Equal(t, http.StatusOK, repoResponse.StatusCode(), "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
conf.Storage.GC = false
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 := CreateRandomImage()
content := img.ManifestDescriptor.Data
digest := img.ManifestDescriptor.Digest
err := 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)
signature.NotationPathLock.Lock()
defer signature.NotationPathLock.Unlock()
signature.LoadNotationPath(tdir)
err = signature.GenerateNotationCerts(tdir, "good")
So(err, ShouldBeNil)
err = signature.GenerateNotationCerts(tdir, "bad")
So(err, ShouldBeNil)
image := fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")
err = signature.SignWithNotation("good", image, tdir, true)
So(err, ShouldBeNil)
err = signature.VerifyWithNotation(image, tdir)
So(err, ShouldBeNil)
// check list
sigs, err := signature.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", ispec.MediaTypeImageManifest).
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 = signature.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 = signature.VerifyWithNotation(image, tdir)
So(err, ShouldNotBeNil)
})
})
})
}
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 := CreateRandomImage()
content := img.ManifestDescriptor.Data
digest := img.ManifestDescriptor.Digest
configDigest := img.ConfigDescriptor.Digest
configBlob := img.ConfigDescriptor.Data
err := 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"
image := CreateImageWith().RandomLayers(1, 2).DefaultConfig().Build()
digest := image.ManifestDescriptor.Digest
err := UploadImage(image, baseURL, repoName, "1.0")
So(err, ShouldBeNil)
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 := getEmptyImageConfig()
resp, err = resty.R().
SetContentLength(true).
SetHeader("Content-Length", strconv.Itoa(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: image.ManifestDescriptor.Size,
},
},
Subject: &ispec.Descriptor{
MediaType: ispec.MediaTypeImageManifest,
Digest: digest,
Size: image.ManifestDescriptor.Size,
},
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", strconv.Itoa(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") //nolint:canonicalheader
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:canonicalheader
})
})
})
}
//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", "-1")
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("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)
})
})
}
func TestListingTags(t *testing.T) {
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)
img := CreateRandomImage()
sigTag := fmt.Sprintf("sha256-%s.sig", img.Digest().Encoded())
repoName := "test-tags"
tagsList := []string{
"1", "2", "1.0.0", "new", "2.0.0", sigTag,
"2-test", "New", "2.0.0-test", "latest",
}
for _, tag := range tagsList {
err := UploadImage(img, baseURL, repoName, tag)
if err != nil {
panic(err)
}
}
// Note empty strings signify the query parameter is not set
// There are separate tests for passing the empty string as query parameter
testCases := []struct {
testCaseName string
pageSize string
last string
expectedTags []string
}{
{
testCaseName: "Test tag soting order with no parameters",
pageSize: "",
last: "",
expectedTags: []string{
"1", "1.0.0", "2", "2-test", "2.0.0", "2.0.0-test",
"New", "latest", "new", sigTag,
},
},
{
testCaseName: "Test with the parameter 'n' lower than total number of results",
pageSize: "5",
last: "",
expectedTags: []string{
"1", "1.0.0", "2", "2-test", "2.0.0",
},
},
{
testCaseName: "Test with the parameter 'n' larger than total number of results",
pageSize: "50",
last: "",
expectedTags: []string{
"1", "1.0.0", "2", "2-test", "2.0.0", "2.0.0-test",
"New", "latest", "new", sigTag,
},
},
{
testCaseName: "Test the parameter 'n' is 0",
pageSize: "0",
last: "",
expectedTags: []string{},
},
{
testCaseName: "Test the parameters 'n' and 'last'",
pageSize: "5",
last: "2-test",
expectedTags: []string{"2.0.0", "2.0.0-test", "New", "latest", "new"},
},
{
testCaseName: "Test the parameters 'n' and 'last' with `n` exceeding total number of results",
pageSize: "5",
last: "latest",
expectedTags: []string{"new", sigTag},
},
{
testCaseName: "Test the parameter 'n' and 'last' being second to last tag as value",
pageSize: "2",
last: "new",
expectedTags: []string{sigTag},
},
{
testCaseName: "Test the parameter 'last' without parameter 'n'",
pageSize: "",
last: "2",
expectedTags: []string{
"2-test", "2.0.0", "2.0.0-test",
"New", "latest", "new", sigTag,
},
},
{
testCaseName: "Test the parameter 'last' with the final tag as value",
pageSize: "",
last: sigTag,
expectedTags: []string{},
},
{
testCaseName: "Test the parameter 'last' with the second to last tag as value",
pageSize: "",
last: "new",
expectedTags: []string{sigTag},
},
}
for _, testCase := range testCases {
Convey(testCase.testCaseName, t, func() {
t.Log("Running " + testCase.testCaseName)
request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
request = mux.SetURLVars(request, map[string]string{"name": repoName})
if testCase.pageSize != "" || testCase.last != "" {
qparm := request.URL.Query()
if testCase.pageSize != "" {
qparm.Add("n", testCase.pageSize)
}
if testCase.last != "" {
qparm.Add("last", testCase.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.StatusOK)
var tags common.ImageTags
err := json.NewDecoder(resp.Body).Decode(&tags)
So(err, ShouldBeNil)
So(tags.Tags, ShouldEqual, testCase.expectedTags)
alltags := tagsList
sort.Strings(alltags)
actualLinkValue := resp.Header.Get("Link")
if testCase.pageSize == "0" || testCase.pageSize == "" { //nolint:gocritic
So(actualLinkValue, ShouldEqual, "")
} else if testCase.expectedTags[len(testCase.expectedTags)-1] == alltags[len(alltags)-1] {
So(actualLinkValue, ShouldEqual, "")
} else {
expectedLinkValue := fmt.Sprintf("</v2/%s/tags/list?n=%s&last=%s>; rel=\"next\"",
repoName, testCase.pageSize, tags.Tags[len(tags.Tags)-1],
)
So(actualLinkValue, ShouldEqual, expectedLinkValue)
}
t.Log("Finished " + testCase.testCaseName)
})
}
}
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("Manifest deletion deletes all tags", func() {
_, _ = Print("\nManifests")
// check a non-existent manifest
resp, err := resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
Head(baseURL + "/v2/unknown/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
image := CreateImageWith().RandomLayers(1, 2).DefaultConfig().Build()
repoName := "repo7"
err = UploadImage(image, baseURL, repoName, "test:1.0")
So(err, ShouldBeNil)
_, err = os.Stat(path.Join(dir, "repo7"))
So(err, ShouldBeNil)
content := image.ManifestDescriptor.Data
digest := image.ManifestDescriptor.Digest
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())
err = UploadImage(image, baseURL, repoName, "test:1.0.1")
So(err, ShouldBeNil)
image = CreateImageWith().RandomLayers(1, 1).DefaultConfig().Build()
err = UploadImage(image, baseURL, repoName, "test:2.0")
So(err, ShouldBeNil)
// update tag to match the other manifest
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)
// the manifest should still be present in storage
_, err = os.Stat(path.Join(dir, "repo7", "blobs", digest.Algorithm().String(),
digest.Encoded()))
So(err, ShouldBeNil)
// the index should not longer contain this tag
tags, err := readTagsFromStorage(dir, "repo7", digest)
So(err, ShouldBeNil)
So(tags, ShouldNotContain, "test:1.0")
So(tags, ShouldContain, "test:1.0.1")
So(tags, ShouldContain, "test:2.0")
So(len(tags), ShouldEqual, 2)
// 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)
// the manifest should be removed from storage
_, err = os.Stat(path.Join(dir, "repo7", "blobs", digest.Algorithm().String(),
digest.Encoded()))
So(err, ShouldNotBeNil)
// the index should not longer contain this manifest
tags, err = readTagsFromStorage(dir, "repo7", digest)
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 0)
// 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)
})
Convey("Deleting all tags does not remove the manifest", func() {
_, _ = Print("\nManifests")
// check a non-existent manifest
resp, err := resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
Head(baseURL + "/v2/unknown/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
image := CreateImageWith().RandomLayers(1, 2).DefaultConfig().Build()
repoName := "repo7"
err = UploadImage(image, baseURL, repoName, "test:1.0")
So(err, ShouldBeNil)
_, err = os.Stat(path.Join(dir, "repo7"))
So(err, ShouldBeNil)
content := image.ManifestDescriptor.Data
digest := image.ManifestDescriptor.Digest
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())
err = UploadImage(image, baseURL, repoName, "test:1.0.1")
So(err, ShouldBeNil)
image = CreateImageWith().RandomLayers(1, 1).DefaultConfig().Build()
err = UploadImage(image, baseURL, repoName, "test:2.0")
So(err, ShouldBeNil)
// update tag to match the other manifest
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)
// the manifest should still be present in storage
_, err = os.Stat(path.Join(dir, "repo7", "blobs", digest.Algorithm().String(),
digest.Encoded()))
So(err, ShouldBeNil)
// the index should not longer contain this tag
tags, err := readTagsFromStorage(dir, "repo7", digest)
So(err, ShouldBeNil)
So(tags, ShouldNotContain, "test:1.0")
So(tags, ShouldContain, "test:1.0.1")
So(tags, ShouldContain, "test:2.0")
So(len(tags), ShouldEqual, 2)
// delete manifest by tag should pass
resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/test:1.0.1")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
// the manifest should still be present in storage
_, err = os.Stat(path.Join(dir, "repo7", "blobs", digest.Algorithm().String(),
digest.Encoded()))
So(err, ShouldBeNil)
// the index should not longer contain this manifest
tags, err = readTagsFromStorage(dir, "repo7", digest)
So(err, ShouldBeNil)
So(tags, ShouldNotContain, "test:1.0")
So(tags, ShouldNotContain, "test:1.0.1")
So(tags, ShouldContain, "test:2.0")
So(len(tags), ShouldEqual, 1)
// delete manifest by tag should pass
resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/test:2.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
// the manifest should still be present in storage
_, err = os.Stat(path.Join(dir, "repo7", "blobs", digest.Algorithm().String(),
digest.Encoded()))
So(err, ShouldBeNil)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, "repo7", digest)
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// 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.StatusOK)
resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/" + digest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
So(resp.Body(), ShouldNotBeEmpty)
// if the only index entry is for the empty name,
// adding a new tag would replace that index entry
// instead of having 2 entries
err = UploadImage(image, baseURL, repoName, "test:2.0.1")
So(err, ShouldBeNil)
// update tag to match the other manifest
resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
SetBody(content).Put(baseURL + "/v2/repo7/manifests/test:2.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())
// the index should not longer contain this tag
tags, err = readTagsFromStorage(dir, "repo7", digest)
So(err, ShouldBeNil)
So(tags, ShouldContain, "test:2.0.1")
So(len(tags), ShouldEqual, 1)
// delete manifest by tag should pass
resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/test:2.0.1")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
// the manifest should still be present in storage
_, err = os.Stat(path.Join(dir, "repo7", "blobs", digest.Algorithm().String(),
digest.Encoded()))
So(err, ShouldBeNil)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, "repo7", digest)
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/" + digest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
})
})
}
func TestMultiarchImage(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()
Convey("Manifests used within an index should not be deletable", func() {
repoName := "multiarch" //nolint:goconst
image1 := CreateRandomImage()
image2 := CreateRandomImage()
multiImage := CreateMultiarchWith().Images([]Image{image1, image2}).Build()
err := UploadMultiarchImage(multiImage, baseURL, repoName, "index1")
So(err, ShouldBeNil)
// add another tag for one of the manifests
manifestBlob, err := json.Marshal(image2.Manifest)
So(err, ShouldBeNil)
resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
SetBody(manifestBlob).Put(baseURL + "/v2/" + repoName + "/manifests/manifest2")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/index1")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/manifest2")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + image1.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + image2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// the repo should contain this index with a tag
tags, err := readTagsFromStorage(dir, repoName, multiImage.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "index1")
So(len(tags), ShouldEqual, 1)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, repoName, image1.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, repoName, image2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "manifest2")
So(len(tags), ShouldEqual, 1)
// delete manifest by digest should fail
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + image1.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + image2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
tags, err = readTagsFromStorage(dir, repoName, image2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "manifest2")
So(len(tags), ShouldEqual, 1)
// delete manifest tag should work
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/manifest2")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
tags, err = readTagsFromStorage(dir, repoName, image2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// the manifest should not be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(image2.Digest().Algorithm()),
image2.Digest().Encoded()))
So(err, ShouldBeNil)
// delete tag of index should pass, but the index should be there
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/index1")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
// the index should not be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(multiImage.Digest().Algorithm()),
multiImage.Digest().Encoded()))
So(err, ShouldBeNil)
// The index should not be available by tag
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/index1")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
// The index should still be available by digest
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + multiImage.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// the repo should contain this index without a tag
tags, err = readTagsFromStorage(dir, repoName, multiImage.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, repoName, image1.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, repoName, image2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// add another tag
indexBlob, err := json.Marshal(multiImage.Index)
So(err, ShouldBeNil)
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
SetBody(indexBlob).Put(baseURL + "/v2/" + repoName + "/manifests/index2")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
// the repo should contain this index with just the new tag
tags, err = readTagsFromStorage(dir, repoName, multiImage.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "index2")
So(len(tags), ShouldEqual, 1)
// delete tag of index should pass, but the index should be there
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/index2")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
// the repo should contain this index without a tag
tags, err = readTagsFromStorage(dir, repoName, multiImage.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// delete index by digest
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + multiImage.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
// the index should not be available by digest
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + multiImage.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
// the index should be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(multiImage.Digest().Algorithm()),
multiImage.Digest().Encoded()))
So(err, ShouldNotBeNil)
// the repo should contain this index without a tag
tags, err = readTagsFromStorage(dir, repoName, multiImage.Digest())
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 0)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + image1.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + image2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// the manifest should not be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(image1.Digest().Algorithm()),
image1.Digest().Encoded()))
So(err, ShouldBeNil)
// the manifest should not be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(image2.Digest().Algorithm()),
image2.Digest().Encoded()))
So(err, ShouldBeNil)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, repoName, image1.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, repoName, image2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// delete manifest should succeed
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + image1.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + image2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + image1.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + image2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
tags, err = readTagsFromStorage(dir, repoName, image1.Digest())
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 0)
tags, err = readTagsFromStorage(dir, repoName, image2.Digest())
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 0)
// the manifest should be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(image1.Digest().Algorithm()),
image1.Digest().Encoded()))
So(err, ShouldNotBeNil)
// the manifest should be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(image2.Digest().Algorithm()),
image2.Digest().Encoded()))
So(err, ShouldNotBeNil)
})
Convey("Manifests used within multiple indexes should not be deletable", func() {
repoName := "multiarch"
image1 := CreateRandomImage()
image2 := CreateRandomImage()
image3 := CreateRandomImage()
multiImage1 := CreateMultiarchWith().Images([]Image{image1, image2}).Build()
multiImage2 := CreateMultiarchWith().Images([]Image{image2, image3}).Build()
err := UploadMultiarchImage(multiImage1, baseURL, repoName, "index1")
So(err, ShouldBeNil)
err = UploadMultiarchImage(multiImage2, baseURL, repoName, "index2")
So(err, ShouldBeNil)
// add another tag for one of the indexes
indexBlob, err := json.Marshal(multiImage2.Index)
So(err, ShouldBeNil)
resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
SetBody(indexBlob).Put(baseURL + "/v2/" + repoName + "/manifests/index22")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/index1")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/index2")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/index22")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + image1.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + image2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// the repo should contain this index with a tag
tags, err := readTagsFromStorage(dir, repoName, multiImage1.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "index1")
So(len(tags), ShouldEqual, 1)
// the repo should contain this index with multiple tags
tags, err = readTagsFromStorage(dir, repoName, multiImage2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "index2")
So(tags, ShouldContain, "index22")
So(len(tags), ShouldEqual, 2)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, repoName, image1.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, repoName, image2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, repoName, image3.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// delete manifest by digest should fail
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + image2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
tags, err = readTagsFromStorage(dir, repoName, image2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// the manifest should not be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(image2.Digest().Algorithm()),
image2.Digest().Encoded()))
So(err, ShouldBeNil)
// delete tag of index should pass, but the index should be there
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/index2")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
// the index should not be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(multiImage2.Digest().Algorithm()),
multiImage2.Digest().Encoded()))
So(err, ShouldBeNil)
// The index should not be available by tag
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/index2")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
// The index should still be available by digest and the other tag
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/index22")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + multiImage2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// the repo should contain this index with just 1 tag
tags, err = readTagsFromStorage(dir, repoName, multiImage2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "index22")
So(len(tags), ShouldEqual, 1)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, repoName, image1.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, repoName, image2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, repoName, image3.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// add another tag
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
SetBody(indexBlob).Put(baseURL + "/v2/" + repoName + "/manifests/index222")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
// the repo should contain this index with both old and new tags
tags, err = readTagsFromStorage(dir, repoName, multiImage2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "index22")
So(tags, ShouldContain, "index222")
So(len(tags), ShouldEqual, 2)
// delete manifest by digest should fail
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + image2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
// delete 1st index by digest
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + multiImage1.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
// the index should not be available by digest
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + multiImage1.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
// the index should be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(multiImage1.Digest().Algorithm()),
multiImage1.Digest().Encoded()))
So(err, ShouldNotBeNil)
// the repo should contain this index without a tag
tags, err = readTagsFromStorage(dir, repoName, multiImage1.Digest())
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 0)
// the repo should contain this index with 2 tags
tags, err = readTagsFromStorage(dir, repoName, multiImage2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "index22")
So(tags, ShouldContain, "index222")
So(len(tags), ShouldEqual, 2)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + image1.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + image2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + image3.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + multiImage2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// the manifest should not be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(image2.Digest().Algorithm()),
image2.Digest().Encoded()))
So(err, ShouldBeNil)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, repoName, image2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// delete manifest should succeed for only 1 manifest
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + image1.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
// this manifest is referenced by the other index
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + image2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
// this manifest is referenced by the other index
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + image3.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
tags, err = readTagsFromStorage(dir, repoName, image1.Digest())
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 0)
tags, err = readTagsFromStorage(dir, repoName, image2.Digest())
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 1)
tags, err = readTagsFromStorage(dir, repoName, image3.Digest())
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 1)
// the manifest should be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(image1.Digest().Algorithm()),
image1.Digest().Encoded()))
So(err, ShouldNotBeNil)
// the manifest should not be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(image2.Digest().Algorithm()),
image2.Digest().Encoded()))
So(err, ShouldBeNil)
// delete 2nd index by digest
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + multiImage2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
// the repo should not contain this index
tags, err = readTagsFromStorage(dir, repoName, multiImage2.Digest())
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 0)
// the index should be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(multiImage2.Digest().Algorithm()),
multiImage2.Digest().Encoded()))
So(err, ShouldNotBeNil)
// delete manifest should succeed
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + image2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + image3.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + image2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + image3.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
tags, err = readTagsFromStorage(dir, repoName, image2.Digest())
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 0)
tags, err = readTagsFromStorage(dir, repoName, image3.Digest())
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 0)
// the manifest should be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(image2.Digest().Algorithm()),
image2.Digest().Encoded()))
So(err, ShouldNotBeNil)
// the manifest should be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(image3.Digest().Algorithm()),
image3.Digest().Encoded()))
So(err, ShouldNotBeNil)
})
Convey("Indexes referenced within other indexes should not be deleted", func() {
repoName := "multiarch"
image1 := CreateRandomImage()
image2 := CreateRandomImage()
index1 := CreateMultiarchWith().Images([]Image{image1}).Build()
index2 := CreateMultiarchWith().Images([]Image{image2}).Build()
err := UploadMultiarchImage(index1, baseURL, repoName, "index1")
So(err, ShouldBeNil)
err = UploadMultiarchImage(index2, baseURL, repoName, index2.Digest().String())
So(err, ShouldBeNil)
rootIndex := ispec.Index{
Versioned: specs.Versioned{SchemaVersion: 2},
MediaType: ispec.MediaTypeImageIndex,
Manifests: []ispec.Descriptor{
{
Digest: index1.IndexDescriptor.Digest,
Size: index1.IndexDescriptor.Size,
MediaType: ispec.MediaTypeImageIndex,
},
{
Digest: index2.IndexDescriptor.Digest,
Size: index2.IndexDescriptor.Size,
MediaType: ispec.MediaTypeImageIndex,
},
},
}
indexBlob, err := json.Marshal(rootIndex)
So(err, ShouldBeNil)
rootIndexDigest := godigest.FromBytes(indexBlob)
resp, err := resty.R().
SetHeader("Content-type", ispec.MediaTypeImageIndex).
SetBody(indexBlob).
Put(baseURL + "/v2/" + repoName + "/manifests/root")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/index1")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/root")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Get(baseURL + "/v2/" + repoName + "/manifests/index1")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Get(baseURL + "/v2/" + repoName + "/manifests/root")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
So(resp.Body(), ShouldNotBeEmpty)
So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + index1.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + index2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + rootIndexDigest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Get(baseURL + "/v2/" + repoName + "/manifests/" + rootIndexDigest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
So(resp.Body(), ShouldNotBeEmpty)
So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
// the repo should contain this index with a tag
tags, err := readTagsFromStorage(dir, repoName, index1.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "index1")
So(len(tags), ShouldEqual, 1)
// the repo should contain this index with a tag
tags, err = readTagsFromStorage(dir, repoName, index2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// the repo should contain this index with a tag
tags, err = readTagsFromStorage(dir, repoName, rootIndexDigest)
So(err, ShouldBeNil)
So(tags, ShouldContain, "root")
So(len(tags), ShouldEqual, 1)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, repoName, image1.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, repoName, image2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// delete manifest by digest should fail
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + image1.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
tags, err = readTagsFromStorage(dir, repoName, image1.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(image1.Digest().Algorithm()),
image1.Digest().Encoded()))
So(err, ShouldBeNil)
// delete index by digest should fail as it is referenced in the root index
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + index1.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
tags, err = readTagsFromStorage(dir, repoName, index1.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "index1")
So(len(tags), ShouldEqual, 1)
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(index1.Digest().Algorithm()),
index1.Digest().Encoded()))
So(err, ShouldBeNil)
// delete index by digest should fail as it is referenced in the root index
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + index2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
tags, err = readTagsFromStorage(dir, repoName, index2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(index2.Digest().Algorithm()),
index2.Digest().Encoded()))
So(err, ShouldBeNil)
// delete tag of referenced index should pass, but the index should be there
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/index1")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
// the index should not be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(index1.Digest().Algorithm()),
index1.Digest().Encoded()))
So(err, ShouldBeNil)
// The index should not be available by tag
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/index1")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
// The index should still be available by digest
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + index1.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// the repo should contain this index with just 1 tag
tags, err = readTagsFromStorage(dir, repoName, index1.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, repoName, index2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, repoName, rootIndexDigest)
So(err, ShouldBeNil)
So(tags, ShouldContain, "root")
So(len(tags), ShouldEqual, 1)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, repoName, image1.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// the index should contain this manifest without any tags
tags, err = readTagsFromStorage(dir, repoName, image2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// add another tag to the second index
index2Blob, err := json.Marshal(index2.Index)
So(err, ShouldBeNil)
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
SetBody(index2Blob).Put(baseURL + "/v2/" + repoName + "/manifests/index2")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
// the index should contain this manifest with a single tag
tags, err = readTagsFromStorage(dir, repoName, index2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "index2")
So(len(tags), ShouldEqual, 1)
// delete tag of referenced index should pass, but the index should be there
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/index2")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
// the index should not be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(index2.Digest().Algorithm()),
index2.Digest().Encoded()))
So(err, ShouldBeNil)
// The index should not be available by tag
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/index2")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
// The index should still be available by digest
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + index2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// the index should contain this manifest with a single tag
tags, err = readTagsFromStorage(dir, repoName, index2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// delete manifest by digest should fail
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + index2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
// add another tag to the root index
resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
SetBody(indexBlob).Put(baseURL + "/v2/" + repoName + "/manifests/root2")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
// the repo should contain this index with both old and new tags
tags, err = readTagsFromStorage(dir, repoName, rootIndexDigest)
So(err, ShouldBeNil)
So(tags, ShouldContain, "root")
So(tags, ShouldContain, "root2")
So(len(tags), ShouldEqual, 2)
// delete 1st root tag
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/root")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
// the repo should contain this index with only the new tag
tags, err = readTagsFromStorage(dir, repoName, rootIndexDigest)
So(err, ShouldBeNil)
So(tags, ShouldContain, "root2")
So(len(tags), ShouldEqual, 1)
// the index should be available by digest
resp, err = resty.R().Get(baseURL + "/v2/" + repoName + "/manifests/" + rootIndexDigest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
So(resp.Body(), ShouldNotBeEmpty)
So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
// delete root index by digest
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + rootIndexDigest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
// the index should not be available by digest
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + rootIndexDigest.String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
// the index should be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(rootIndexDigest.Algorithm()),
rootIndexDigest.Encoded()))
So(err, ShouldNotBeNil)
// the repo should not contain this root index
tags, err = readTagsFromStorage(dir, repoName, rootIndexDigest)
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 0)
// the repo should contain this index with no tags
tags, err = readTagsFromStorage(dir, repoName, index1.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// the repo should contain this index with no tags
tags, err = readTagsFromStorage(dir, repoName, index2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// the index should not be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(index1.Digest().Algorithm()),
index1.Digest().Encoded()))
So(err, ShouldBeNil)
// the index should not be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(index2.Digest().Algorithm()),
index2.Digest().Encoded()))
So(err, ShouldBeNil)
// the single arch images should continue to be available and we should not be allowed to delete them
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + image1.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + image2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
// the manifest should not be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(image1.Digest().Algorithm()),
image1.Digest().Encoded()))
So(err, ShouldBeNil)
// the manifest should not be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(image2.Digest().Algorithm()),
image2.Digest().Encoded()))
So(err, ShouldBeNil)
// delete index1 by digest
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + index1.Digest().String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
// the index should not be available by digest
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + index1.Digest().String())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
// the index should be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(index1.Digest().Algorithm()),
index1.Digest().Encoded()))
So(err, ShouldNotBeNil)
// the repo should not contain this index
tags, err = readTagsFromStorage(dir, repoName, index1.Digest())
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 0)
// delete manifest should succeed for only 1 manifest
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + image1.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
// this manifest is referenced by the other index
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + image2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
tags, err = readTagsFromStorage(dir, repoName, image1.Digest())
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 0)
tags, err = readTagsFromStorage(dir, repoName, image2.Digest())
So(err, ShouldBeNil)
So(tags, ShouldContain, "")
So(len(tags), ShouldEqual, 1)
// the manifest should be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(image1.Digest().Algorithm()),
image1.Digest().Encoded()))
So(err, ShouldNotBeNil)
// the manifest should not be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(image2.Digest().Algorithm()),
image2.Digest().Encoded()))
So(err, ShouldBeNil)
// delete 2nd index by digest
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + index2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
// the repo should not contain this index
tags, err = readTagsFromStorage(dir, repoName, index2.Digest())
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 0)
// the index should be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(index2.Digest().Algorithm()),
index2.Digest().Encoded()))
So(err, ShouldNotBeNil)
// delete manifest should succeed
resp, err = resty.R().Delete(baseURL + "/v2/" + repoName + "/manifests/" + image2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
resp, err = resty.R().Head(baseURL + "/v2/" + repoName + "/manifests/" + image2.DigestStr())
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
tags, err = readTagsFromStorage(dir, repoName, image2.Digest())
So(err, ShouldBeNil)
So(len(tags), ShouldEqual, 0)
// the manifest should be removed from storage
_, err = os.Stat(path.Join(dir, repoName, "blobs", string(image2.Digest().Algorithm()),
image2.Digest().Encoded()))
So(err, ShouldNotBeNil)
})
})
}
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)
img := CreateImageWith().RandomLayers(1, 2).DefaultConfig().Build()
// check a non-existent manifest
resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
Head(baseURL + "/v2/unknown/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
repoName := "index"
err = UploadImage(img, baseURL, repoName, "test:1.0")
So(err, ShouldBeNil)
_, err = os.Stat(path.Join(dir, "index"))
So(err, ShouldBeNil)
content := img.ManifestDescriptor.Data
digest := img.ManifestDescriptor.Digest
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 = CreateRandomImage()
err = 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 := CreateRandomImage()
err = 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 + "/v2/index/manifests/" + 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 = CreateRandomImage()
err = 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 + "/v2/index/manifests/" + 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 common.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 + "/v2/index/manifests/" + 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)
// Manifest is in use, tag is deleted, but manifest is not
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
So(resp.Body(), ShouldBeEmpty)
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 := CreateRandomImage()
err = 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{
test.AuthorizationAllRepos: config.PolicyGroup{
AnonymousPolicy: []string{
constants.ReadPermission,
constants.CreatePermission,
constants.DeletePermission,
constants.DetectManifestCollisionPermission,
},
},
},
}
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
img := CreateImageWith().RandomLayers(1, 2).DefaultConfig().Build()
err := UploadImage(img, baseURL, "index", "test:1.0")
So(err, ShouldBeNil)
_, err = os.Stat(path.Join(dir, "index"))
So(err, ShouldBeNil)
// check a non-existent manifest
resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
Head(baseURL + "/v2/unknown/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
digest := img.ManifestDescriptor.Digest
So(digest, ShouldNotBeNil)
err = UploadImage(img, 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[test.AuthorizationAllRepos]
repoPolicy.AnonymousPolicy = []string{"read", "delete"}
conf.HTTP.AccessControl.Repositories[test.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, strconv.Itoa(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, strconv.Itoa(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, strconv.Itoa(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 := GetRandomImageConfig()
resp, err = resty.R().
SetContentLength(true).
SetHeader("Content-Length", strconv.Itoa(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", strconv.Itoa(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 := GetRandomImageConfig()
resp, err = resty.R().
SetContentLength(true).
SetHeader("Content-Length", strconv.Itoa(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 TestGCSignaturesAndUntaggedManifestsWithMetaDB(t *testing.T) {
ctx := context.Background()
Convey("Make controller", t, func() {
trueVal := true
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
logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil)
conf.Log.Audit = logFile.Name()
value := true
searchConfig := &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &value},
}
// added search extensions so that metaDB is initialized and its tested in GC logic
conf.Extensions = &extconf.ExtensionConfig{
Search: searchConfig,
}
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.Retention = config.ImageRetention{
Delay: 1 * time.Millisecond,
Policies: []config.RetentionPolicy{
{
Repositories: []string{"**"},
DeleteReferrers: true,
DeleteUntagged: &trueVal,
KeepTags: []config.KeepTagsPolicy{
{
Patterns: []string{".*"}, // just for coverage
},
},
},
},
}
ctlr.Config.Storage.Dedupe = false
cm := test.NewControllerManager(ctlr)
cm.StartServer() //nolint: contextcheck
cm.WaitServerToBeReady(port)
defer cm.StopServer()
img := CreateDefaultImage()
err = UploadImage(img, baseURL, repoName, tag)
So(err, ShouldBeNil)
gc := gc.NewGarbageCollect(ctlr.StoreController.DefaultStore, ctlr.MetaDB,
gc.Options{
Delay: ctlr.Config.Storage.GCDelay,
ImageRetention: ctlr.Config.Storage.Retention,
}, ctlr.Audit, ctlr.Log)
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(ctx, "", "cosign", nil)
So(err, ShouldBeNil)
image := fmt.Sprintf("localhost:%s/%s@%s", port, repoName, digest.String())
annotations := []string{"tag=" + 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)
signature.NotationPathLock.Lock()
defer signature.NotationPathLock.Unlock()
signature.LoadNotationPath(tdir)
// generate a keypair
err = signature.GenerateNotationCerts(tdir, "good")
So(err, ShouldBeNil)
// sign the image
err = signature.SignWithNotation("good", image, tdir, true) //nolint: contextcheck
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)
cosignDigest := resp.Header().Get(constants.DistContentDigestKey)
So(cosignDigest, ShouldNotBeEmpty)
// 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)
// shouldn't do anything
err = gc.CleanRepo(ctx, repoName) //nolint: contextcheck
So(err, ShouldBeNil)
// make sure both signatures are stored in repodb
repoMeta, err := ctlr.MetaDB.GetRepoMeta(ctx, repoName)
So(err, ShouldBeNil)
sigMeta := repoMeta.Signatures[digest.String()]
So(len(sigMeta[storage.CosignType]), ShouldEqual, 1)
So(len(sigMeta[storage.NotationType]), ShouldEqual, 1)
So(sigMeta[storage.CosignType][0].SignatureManifestDigest, ShouldEqual, cosignDigest)
So(sigMeta[storage.NotationType][0].SignatureManifestDigest, ShouldEqual, index.Manifests[0].Digest.String())
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 := CreateRandomImage()
err = UploadImage(img, baseURL, repoName, img.DigestStr())
So(err, ShouldBeNil)
err = gc.CleanRepo(ctx, 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 = UploadImage(img, baseURL, repoName, tag)
So(err, ShouldBeNil)
err = gc.CleanRepo(ctx, 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
img := CreateImageWith().RandomLayers(1, 2).DefaultConfig().Build()
untaggedManifestDigest := img.ManifestDescriptor.Digest
err = UploadImage(img, baseURL, repoName, untaggedManifestDigest.String())
So(err, ShouldBeNil)
// make sure repoDB reference was added
repoMeta, err := ctlr.MetaDB.GetRepoMeta(ctx, repoName)
So(err, ShouldBeNil)
_, ok := repoMeta.Referrers[untaggedManifestDigest.String()]
So(ok, ShouldBeTrue)
_, ok = repoMeta.Signatures[untaggedManifestDigest.String()]
So(ok, ShouldBeTrue)
_, ok = repoMeta.Statistics[untaggedManifestDigest.String()]
So(ok, ShouldBeTrue)
// overwrite image so that signatures will get invalidated and gc'ed
img = CreateImageWith().RandomLayers(1, 3).DefaultConfig().Build()
err = UploadImage(img, baseURL, repoName, tag)
So(err, ShouldBeNil)
newManifestDigest := img.ManifestDescriptor.Digest
err = gc.CleanRepo(ctx, repoName) //nolint: contextcheck
So(err, ShouldBeNil)
// make sure both signatures are removed from metaDB and repo reference for untagged is removed
repoMeta, err = ctlr.MetaDB.GetRepoMeta(ctx, repoName)
So(err, ShouldBeNil)
sigMeta := repoMeta.Signatures[digest.String()]
So(len(sigMeta[storage.CosignType]), ShouldEqual, 0)
So(len(sigMeta[storage.NotationType]), ShouldEqual, 0)
_, ok = repoMeta.Referrers[untaggedManifestDigest.String()]
So(ok, ShouldBeFalse)
_, ok = repoMeta.Signatures[untaggedManifestDigest.String()]
So(ok, ShouldBeFalse)
_, ok = repoMeta.Statistics[untaggedManifestDigest.String()]
So(ok, ShouldBeFalse)
// 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.Retention = config.ImageRetention{
Delay: 1 * time.Second,
Policies: []config.RetentionPolicy{
{
Repositories: []string{"**"},
DeleteReferrers: true,
DeleteUntagged: &trueVal,
},
},
}
err := WriteImageToFileSystem(CreateDefaultImage(), repoName, tag,
ociutils.GetDefaultStoreController(dir, ctlr.Log))
So(err, ShouldBeNil)
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(port) //nolint: contextcheck
defer cm.StopServer()
gc := gc.NewGarbageCollect(ctlr.StoreController.DefaultStore, ctlr.MetaDB,
gc.Options{
Delay: ctlr.Config.Storage.GCDelay,
ImageRetention: ctlr.Config.Storage.Retention,
}, ctlr.Audit, ctlr.Log)
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++ {
img := CreateImageWith().RandomLayers(1, 1000+i).DefaultConfig().Build()
manifestDigest := img.ManifestDescriptor.Digest
err = UploadImage(img, baseURL, repoName, manifestDigest.String())
So(err, ShouldBeNil)
index.Manifests = append(index.Manifests, ispec.Descriptor{
Digest: manifestDigest,
MediaType: ispec.MediaTypeImageManifest,
Size: img.ManifestDescriptor.Size,
})
}
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 = gc.CleanRepo(ctx, 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 = WriteImageToFileSystem(CreateDefaultImage(), repoName, "0.0.1",
ociutils.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,
"executing gc of orphaned blobs for "+path.Join(ctlr.StoreController.DefaultStore.RootDir(), repoName)) //nolint:lll
So(string(data), ShouldContainSubstring,
"gc successfully completed for "+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,
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 = WriteImageToFileSystem(CreateDefaultImage(), repoName, "0.0.1",
ociutils.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, "failed to walk storage root-dir") //nolint:lll
})
}
func TestSearchRoutes(t *testing.T) {
Convey("Upload image for test", t, func(c C) {
tempDir := t.TempDir()
repoName := "testrepo" //nolint:goconst
inaccessibleRepo := "inaccessible"
Convey("GlobalSearch with authz enabled", func(c C) {
conf := config.New()
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
user1 := "test"
password1 := "test"
testString1 := test.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()
img := CreateImageWith().RandomLayers(1, 10000).DefaultConfig().Build()
err := UploadImageWithBasicAuth(
img, baseURL, repoName, "latest",
user1, password1)
So(err, ShouldBeNil)
// data for the inaccessible repo
img = CreateImageWith().RandomLayers(1, 10000).DefaultConfig().Build()
err = UploadImageWithBasicAuth(
img, 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 := test.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 := CreateRandomImage()
err := 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 := test.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 := CreateRandomImage()
err := 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 := test.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 := CreateRandomImage()
err := 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 := test.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 := CreateRandomImage()
err := 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 := test.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 := CreateRandomImage()
err := 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, seedGroup1 := test.GenerateRandomString()
user1, seedUser1 := test.GenerateRandomString()
password1, seedPass1 := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(user1, password1))
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)
ctlr.Log.Info().Int64("seedUser1", seedUser1).Int64("seedPass1", seedPass1).
Int64("seedGroup1", seedGroup1).Msg("random seed for username,password & group")
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
img := CreateRandomImage()
err := 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, user1, user2 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[test.AuthorizationAllRepos]
repoPolicy.AnonymousPolicy = append(repoPolicy.AnonymousPolicy, "read")
conf.HTTP.AccessControl.Repositories[test.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 user1 to global policy with create permission
repoPolicy.Policies[0].Users = append(repoPolicy.Policies[0].Users, user1)
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", strconv.Itoa(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[test.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 user2"
conf.HTTP.AccessControl.AdminPolicy.Users = append(conf.HTTP.AccessControl.AdminPolicy.Users, user2)
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[test.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, user 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[test.AuthorizationAllRepos].Policies[0].Users = append(conf.HTTP.AccessControl.Repositories[test.AuthorizationAllRepos].Policies[0].Users, user) //nolint:lll // gofumpt conflicts with lll
conf.HTTP.AccessControl.Repositories[test.AuthorizationAllRepos].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[test.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", strconv.Itoa(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[test.AuthorizationAllRepos].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[test.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[test.AuthorizationAllRepos].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[test.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, user) //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", strconv.Itoa(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[test.AuthorizationAllRepos]
repoPolicy.Policies = []config.Policy{}
repoPolicy.DefaultPolicy = []string{}
conf.HTTP.AccessControl.Repositories[test.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{user},
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 := 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", strconv.Itoa(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", strconv.Itoa(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, user)
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", strconv.Itoa(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)
})
}
func TestSupportedDigestAlgorithms(t *testing.T) {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
dir := t.TempDir()
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = dir
ctlr.Config.Storage.Dedupe = false
ctlr.Config.Storage.GC = false
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
Convey("Test SHA512 single-arch image", t, func() {
image := CreateImageWithDigestAlgorithm(godigest.SHA512).
RandomLayers(1, 10).DefaultConfig().Build()
name := "algo-sha512"
tag := "singlearch"
err := UploadImage(image, baseURL, name, tag)
So(err, ShouldBeNil)
client := resty.New()
// The server picks canonical digests when tags are pushed
// See https://github.com/opencontainers/distribution-spec/issues/494
// It would be nice to be able to push tags with other digest algorithms and verify those are returned
// but there is no way to specify a client preference
// so all we can do is verify the correct algorithm is returned
expectedDigestStr := image.DigestForAlgorithm(godigest.Canonical).String()
verifyReturnedManifestDigest(t, client, baseURL, name, tag, expectedDigestStr)
verifyReturnedManifestDigest(t, client, baseURL, name, expectedDigestStr, expectedDigestStr)
})
Convey("Test SHA512 single-arch image pushed by digest", t, func() {
image := CreateImageWithDigestAlgorithm(godigest.SHA512).
RandomLayers(1, 11).DefaultConfig().Build()
name := "algo-sha512-2"
err := UploadImage(image, baseURL, name, image.DigestStr())
So(err, ShouldBeNil)
client := resty.New()
expectedDigestStr := image.DigestForAlgorithm(godigest.SHA512).String()
verifyReturnedManifestDigest(t, client, baseURL, name, expectedDigestStr, expectedDigestStr)
})
Convey("Test SHA384 single-arch image", t, func() {
image := CreateImageWithDigestAlgorithm(godigest.SHA384).
RandomLayers(1, 10).DefaultConfig().Build()
name := "algo-sha384"
tag := "singlearch"
err := UploadImage(image, baseURL, name, tag)
So(err, ShouldBeNil)
client := resty.New()
// The server picks canonical digests when tags are pushed
// See https://github.com/opencontainers/distribution-spec/issues/494
// It would be nice to be able to push tags with other digest algorithms and verify those are returned
// but there is no way to specify a client preference
// so all we can do is verify the correct algorithm is returned
expectedDigestStr := image.DigestForAlgorithm(godigest.Canonical).String()
verifyReturnedManifestDigest(t, client, baseURL, name, tag, expectedDigestStr)
verifyReturnedManifestDigest(t, client, baseURL, name, expectedDigestStr, expectedDigestStr)
})
Convey("Test SHA512 multi-arch image", t, func() {
subImage1 := CreateImageWithDigestAlgorithm(godigest.SHA512).RandomLayers(1, 10).
DefaultConfig().Build()
subImage2 := CreateImageWithDigestAlgorithm(godigest.SHA512).RandomLayers(1, 10).
DefaultConfig().Build()
multiarch := CreateMultiarchWithDigestAlgorithm(godigest.SHA512).
Images([]Image{subImage1, subImage2}).Build()
name := "algo-sha512"
tag := "multiarch"
err := UploadMultiarchImage(multiarch, baseURL, name, tag)
So(err, ShouldBeNil)
client := resty.New()
// The server picks canonical digests when tags are pushed
// See https://github.com/opencontainers/distribution-spec/issues/494
// It would be nice to be able to push tags with other digest algorithms and verify those are returned
// but there is no way to specify a client preference
// so all we can do is verify the correct algorithm is returned
expectedDigestStr := multiarch.DigestForAlgorithm(godigest.Canonical).String()
verifyReturnedManifestDigest(t, client, baseURL, name, tag, expectedDigestStr)
verifyReturnedManifestDigest(t, client, baseURL, name, expectedDigestStr, expectedDigestStr)
// While the expected multiarch manifest digest is always using the canonical algorithm
// the sub-imgage manifest digest can use any algorith
verifyReturnedManifestDigest(t, client, baseURL, name,
subImage1.ManifestDescriptor.Digest.String(), subImage1.ManifestDescriptor.Digest.String())
verifyReturnedManifestDigest(t, client, baseURL, name,
subImage2.ManifestDescriptor.Digest.String(), subImage2.ManifestDescriptor.Digest.String())
})
Convey("Test SHA512 multi-arch image pushed by digest", t, func() {
subImage1 := CreateImageWithDigestAlgorithm(godigest.SHA512).RandomLayers(1, 10).
DefaultConfig().Build()
subImage2 := CreateImageWithDigestAlgorithm(godigest.SHA512).RandomLayers(1, 10).
DefaultConfig().Build()
multiarch := CreateMultiarchWithDigestAlgorithm(godigest.SHA512).
Images([]Image{subImage1, subImage2}).Build()
name := "algo-sha512-2"
t.Log(multiarch.DigestStr())
err := UploadMultiarchImage(multiarch, baseURL, name, multiarch.DigestStr())
So(err, ShouldBeNil)
client := resty.New()
expectedDigestStr := multiarch.DigestForAlgorithm(godigest.SHA512).String()
verifyReturnedManifestDigest(t, client, baseURL, name, expectedDigestStr, expectedDigestStr)
// While the expected multiarch manifest digest is always using the canonical algorithm
// the sub-imgage manifest digest can use any algorith
verifyReturnedManifestDigest(t, client, baseURL, name,
subImage1.ManifestDescriptor.Digest.String(), subImage1.ManifestDescriptor.Digest.String())
verifyReturnedManifestDigest(t, client, baseURL, name,
subImage2.ManifestDescriptor.Digest.String(), subImage2.ManifestDescriptor.Digest.String())
})
Convey("Test SHA384 multi-arch image", t, func() {
subImage1 := CreateImageWithDigestAlgorithm(godigest.SHA384).RandomLayers(1, 10).
DefaultConfig().Build()
subImage2 := CreateImageWithDigestAlgorithm(godigest.SHA384).RandomLayers(1, 10).
DefaultConfig().Build()
multiarch := CreateMultiarchWithDigestAlgorithm(godigest.SHA384).
Images([]Image{subImage1, subImage2}).Build()
name := "algo-sha384"
tag := "multiarch"
err := UploadMultiarchImage(multiarch, baseURL, name, tag)
So(err, ShouldBeNil)
client := resty.New()
// The server picks canonical digests when tags are pushed
// See https://github.com/opencontainers/distribution-spec/issues/494
// It would be nice to be able to push tags with other digest algorithms and verify those are returned
// but there is no way to specify a client preference
// so all we can do is verify the correct algorithm is returned
expectedDigestStr := multiarch.DigestForAlgorithm(godigest.Canonical).String()
verifyReturnedManifestDigest(t, client, baseURL, name, tag, expectedDigestStr)
verifyReturnedManifestDigest(t, client, baseURL, name, expectedDigestStr, expectedDigestStr)
// While the expected multiarch manifest digest is always using the canonical algorithm
// the sub-imgage manifest digest can use any algorith
verifyReturnedManifestDigest(t, client, baseURL, name,
subImage1.ManifestDescriptor.Digest.String(), subImage1.ManifestDescriptor.Digest.String())
verifyReturnedManifestDigest(t, client, baseURL, name,
subImage2.ManifestDescriptor.Digest.String(), subImage2.ManifestDescriptor.Digest.String())
})
}
func verifyReturnedManifestDigest(t *testing.T, client *resty.Client, baseURL, repoName,
reference, expectedDigestStr string,
) {
t.Helper()
t.Logf("Verify Docker-Content-Digest returned for repo %s reference %s is %s",
repoName, reference, expectedDigestStr)
getResponse, err := client.R().Get(fmt.Sprintf("%s/v2/%s/manifests/%s", baseURL, repoName, reference))
So(err, ShouldBeNil)
So(getResponse, ShouldNotBeNil)
So(getResponse.StatusCode(), ShouldEqual, http.StatusOK)
contentDigestStr := getResponse.Header().Get("Docker-Content-Digest")
So(contentDigestStr, ShouldEqual, expectedDigestStr)
getResponse, err = client.R().Head(fmt.Sprintf("%s/v2/%s/manifests/%s", baseURL, repoName, reference))
So(err, ShouldBeNil)
So(getResponse, ShouldNotBeNil)
So(getResponse.StatusCode(), ShouldEqual, http.StatusOK)
contentDigestStr = getResponse.Header().Get("Docker-Content-Digest")
So(contentDigestStr, ShouldEqual, expectedDigestStr)
}
func getEmptyImageConfig() ([]byte, godigest.Digest) {
config := ispec.Image{}
configBlobContent, err := json.MarshalIndent(&config, "", "\t")
if err != nil {
return nil, ""
}
configBlobDigestRaw := godigest.FromBytes(configBlobContent)
return configBlobContent, configBlobDigestRaw
}
func getNumberOfSessions(rootDir string) (int, error) {
rootDirContents, err := os.ReadDir(path.Join(rootDir, "_sessions"))
if err != nil {
return -1, err
}
sessionsNo := 0
for _, file := range rootDirContents {
if !file.IsDir() && strings.HasPrefix(file.Name(), "session_") {
sessionsNo += 1
}
}
return sessionsNo, nil
}
func readTagsFromStorage(rootDir, repoName string, digest godigest.Digest) ([]string, error) {
result := []string{}
indexJSONBuf, err := os.ReadFile(path.Join(rootDir, repoName, "index.json"))
if err != nil {
return result, err
}
var indexJSON ispec.Index
err = json.Unmarshal(indexJSONBuf, &indexJSON)
if err != nil {
return result, err
}
for _, desc := range indexJSON.Manifests {
if desc.Digest != digest {
continue
}
name := desc.Annotations[ispec.AnnotationRefName]
// There is a special case where there is an entry in
// the index.json without tags, in this case name is an empty string
// Also we should not have duplicates
// Do these checks in the actual test cases, not here
result = append(result, name)
}
return result, nil
}