Files
zot/pkg/extensions/extensions_test.go
T
Andrei Aaron dfb5d1df54 fix: make config read/write thread safe (#3432)
* fix: make config read/write thread safe and fix some other similar issues

1. The config config has a lock, and safe methods to update and read the attributes
2. The config has methods to retrieve copies of specific attributes, such as the extyensions config, the auth config, and the authz config.
These are needed, as the config object may mutate in the middle of an auth/authz requests, and we avoid partial configuration being applied for that request.
3. Fix an issue with the monitoring server not stopping when the controller is shut down.
4. Fix an issue with the HTPasswdWatcher not stopping when the background tasks are supposed to finish.
5. Fix some tests using hardcoded ports.

Moved some of the methods which were on the main config to the auth, access control and extension configs

Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
2025-10-18 11:20:58 +03:00

1078 lines
33 KiB
Go

//go:build sync && metrics && mgmt && userprefs && search
// +build sync,metrics,mgmt,userprefs,search
package extensions_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/resty.v1"
"zotregistry.dev/zot/v2/pkg/api"
"zotregistry.dev/zot/v2/pkg/api/config"
"zotregistry.dev/zot/v2/pkg/api/constants"
"zotregistry.dev/zot/v2/pkg/extensions"
extconf "zotregistry.dev/zot/v2/pkg/extensions/config"
syncconf "zotregistry.dev/zot/v2/pkg/extensions/config/sync"
authutils "zotregistry.dev/zot/v2/pkg/test/auth"
test "zotregistry.dev/zot/v2/pkg/test/common"
)
const (
ServerCert = "../../test/data/server.cert"
ServerKey = "../../test/data/server.key"
)
func TestEnableExtension(t *testing.T) {
Convey("Verify log if sync disabled in config", t, func() {
globalDir := t.TempDir()
port := test.GetFreePort()
conf := config.New()
falseValue := false
syncConfig := &syncconf.Config{
Enable: &falseValue,
Registries: []syncconf.RegistryConfig{},
}
// conf.Extensions.Sync.Enable = &falseValue
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Sync = syncConfig
conf.HTTP.Port = port
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
So(err, ShouldBeNil)
conf.Log.Level = "info"
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
ctlr := api.NewController(conf)
ctlrManager := test.NewControllerManager(ctlr)
defer ctlrManager.StopServer()
ctlr.Config.Storage.RootDirectory = globalDir
ctlrManager.StartAndWait(port)
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
So(string(data), ShouldContainSubstring,
"sync config not provided or disabled, so not enabling sync")
})
}
func TestMetricsExtension(t *testing.T) {
Convey("Verify Metrics enabled for storage subpaths", t, func() {
globalDir := t.TempDir()
conf := config.New()
port := test.GetFreePort()
conf.HTTP.Port = port
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
So(err, ShouldBeNil)
defaultValue := true
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Metrics = &extconf.MetricsConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultValue},
Prometheus: &extconf.PrometheusConfig{},
}
conf.Log.Level = "info"
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
ctlr := api.NewController(conf)
ctlrManager := test.NewControllerManager(ctlr)
subPaths := make(map[string]config.StorageConfig)
subPaths["/a"] = config.StorageConfig{
Dedupe: false,
RootDirectory: t.TempDir(),
}
ctlr.Config.Storage.RootDirectory = globalDir
ctlr.Config.Storage.SubPaths = subPaths
ctlrManager.StartAndWait(port)
data, _ := os.ReadFile(logFile.Name())
So(string(data), ShouldContainSubstring, "metrics extension enabled")
})
}
func TestMgmtExtension(t *testing.T) {
globalDir := t.TempDir()
conf := config.New()
port := test.GetFreePort()
conf.HTTP.Port = port
baseURL := test.GetBaseURL(port)
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
if err != nil {
panic(err)
}
mgmtReadyTimeout := 5 * time.Second
defaultValue := true
mockOIDCServer, err := authutils.MockOIDCRun()
if err != nil {
panic(err)
}
defer func() {
err := mockOIDCServer.Shutdown()
if err != nil {
panic(err)
}
}()
mockOIDCConfig := mockOIDCServer.Config()
Convey("Verify mgmt auth info route enabled with htpasswd", t, func() {
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
defer func() {
conf.HTTP.Auth.HTPasswd.Path = ""
os.Remove(htpasswdPath)
}()
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
ctlr := api.NewController(conf)
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
subPaths := make(map[string]config.StorageConfig)
subPaths["/a"] = config.StorageConfig{RootDirectory: t.TempDir()}
ctlr.Config.Storage.RootDirectory = globalDir
ctlr.Config.Storage.SubPaths = subPaths
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
"setting up mgmt routes", mgmtReadyTimeout)
So(err, ShouldBeNil)
defer func() {
if !found {
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
// without credentials
resp, err := resty.R().Patch(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
// without credentials
resp, err = resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
mgmtResp := extensions.StrippedConfig{}
err = json.Unmarshal(resp.Body(), &mgmtResp)
So(err, ShouldBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd, ShouldNotBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd.Path, ShouldEqual, "")
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil)
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
So(mgmtResp.HTTP.Auth.APIKey, ShouldBeFalse)
// with credentials
resp, err = resty.R().SetBasicAuth(username, password).Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
mgmtResp = extensions.StrippedConfig{}
err = json.Unmarshal(resp.Body(), &mgmtResp)
So(err, ShouldBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd, ShouldNotBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd.Path, ShouldEqual, "")
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil)
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
So(mgmtResp.HTTP.Auth.APIKey, ShouldBeFalse)
// with wrong credentials
resp, err = resty.R().SetBasicAuth(username, "wrong").Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
})
Convey("Verify mgmt auth info route enabled with ldap", t, func() {
conf.HTTP.Auth.LDAP = (&config.LDAPConfig{
BaseDN: "basedn",
Address: "ldapexample",
}).SetBindDN("binddn")
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
ctlr := api.NewController(conf)
subPaths := make(map[string]config.StorageConfig)
subPaths["/a"] = config.StorageConfig{RootDirectory: t.TempDir()}
ctlr.Config.Storage.RootDirectory = t.TempDir()
ctlr.Config.Storage.SubPaths = subPaths
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
"setting up mgmt routes", mgmtReadyTimeout)
defer func() {
if !found {
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
// without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
mgmtResp := extensions.StrippedConfig{}
err = json.Unmarshal(resp.Body(), &mgmtResp)
So(err, ShouldBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd.Path, ShouldEqual, "")
// ldap is always nil, htpasswd should be populated when ldap is used
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil)
So(mgmtResp.HTTP.Auth.APIKey, ShouldBeFalse)
})
Convey("Verify mgmt auth info route enabled with ldap + apikey", t, func() {
conf.HTTP.Auth.LDAP = (&config.LDAPConfig{
BaseDN: "basedn",
Address: "ldapexample",
}).SetBindDN("binddn")
conf.HTTP.Auth.APIKey = true
defer func() {
conf.HTTP.Auth.APIKey = false
}()
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
ctlr := api.NewController(conf)
subPaths := make(map[string]config.StorageConfig)
subPaths["/a"] = config.StorageConfig{RootDirectory: t.TempDir()}
ctlr.Config.Storage.RootDirectory = t.TempDir()
ctlr.Config.Storage.SubPaths = subPaths
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
"setting up mgmt routes", mgmtReadyTimeout)
defer func() {
if !found {
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
// without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
mgmtResp := extensions.StrippedConfig{}
err = json.Unmarshal(resp.Body(), &mgmtResp)
So(err, ShouldBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd.Path, ShouldEqual, "")
// ldap is always nil, htpasswd should be populated when ldap is used
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil)
So(mgmtResp.HTTP.Auth.APIKey, ShouldBeTrue)
})
Convey("Verify mgmt auth info route enabled with htpasswd + ldap", t, func() {
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
defer func() {
conf.HTTP.Auth.HTPasswd.Path = ""
os.Remove(htpasswdPath)
}()
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
conf.HTTP.Auth.LDAP = (&config.LDAPConfig{
BaseDN: "basedn",
Address: "ldapexample",
}).SetBindDN("binddn")
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
ctlr := api.NewController(conf)
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
subPaths := make(map[string]config.StorageConfig)
subPaths["/a"] = config.StorageConfig{RootDirectory: t.TempDir()}
ctlr.Config.Storage.RootDirectory = t.TempDir()
ctlr.Config.Storage.SubPaths = subPaths
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
"setting up mgmt routes", mgmtReadyTimeout)
defer func() {
if !found {
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
// without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
mgmtResp := extensions.StrippedConfig{}
err = json.Unmarshal(resp.Body(), &mgmtResp)
So(err, ShouldBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd, ShouldNotBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd.Path, ShouldEqual, "")
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil)
So(mgmtResp.HTTP.Auth.APIKey, ShouldBeFalse)
// with credentials
resp, err = resty.R().SetBasicAuth(username, password).Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
mgmtResp = extensions.StrippedConfig{}
err = json.Unmarshal(resp.Body(), &mgmtResp)
So(err, ShouldBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd, ShouldNotBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd.Path, ShouldEqual, "")
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil)
So(mgmtResp.HTTP.Auth.APIKey, ShouldBeFalse)
})
Convey("Verify mgmt auth info route enabled with htpasswd + ldap + bearer", t, func() {
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
defer func() {
conf.HTTP.Auth.HTPasswd.Path = ""
os.Remove(htpasswdPath)
}()
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
conf.HTTP.Auth.LDAP = (&config.LDAPConfig{
BaseDN: "basedn",
Address: "ldapexample",
}).SetBindDN("binddn")
conf.HTTP.Auth.Bearer = &config.BearerConfig{
Realm: "realm",
Service: "service",
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
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()
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
"setting up mgmt routes", mgmtReadyTimeout)
defer func() {
if !found {
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
// without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
mgmtResp := extensions.StrippedConfig{}
err = json.Unmarshal(resp.Body(), &mgmtResp)
So(err, ShouldBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd, ShouldNotBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd.Path, ShouldEqual, "")
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
So(mgmtResp.HTTP.Auth.Bearer, ShouldNotBeNil)
So(mgmtResp.HTTP.Auth.Bearer.Realm, ShouldEqual, "realm")
So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, "service")
So(mgmtResp.HTTP.Auth.APIKey, ShouldBeFalse)
// with credentials
resp, err = resty.R().SetBasicAuth(username, password).Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
mgmtResp = extensions.StrippedConfig{}
err = json.Unmarshal(resp.Body(), &mgmtResp)
So(err, ShouldBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd, ShouldNotBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd.Path, ShouldEqual, "")
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
So(mgmtResp.HTTP.Auth.Bearer, ShouldNotBeNil)
So(mgmtResp.HTTP.Auth.Bearer.Realm, ShouldEqual, "realm")
So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, "service")
So(mgmtResp.HTTP.Auth.APIKey, ShouldBeFalse)
})
Convey("Verify mgmt auth info route enabled with ldap + bearer", t, func() {
conf.HTTP.Auth.HTPasswd.Path = ""
conf.HTTP.Auth.LDAP = (&config.LDAPConfig{
BaseDN: "basedn",
Address: "ldapexample",
}).SetBindDN("binddn")
conf.HTTP.Auth.Bearer = &config.BearerConfig{
Realm: "realm",
Service: "service",
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
ctlr := api.NewController(conf)
subPaths := make(map[string]config.StorageConfig)
subPaths["/a"] = config.StorageConfig{RootDirectory: t.TempDir()}
ctlr.Config.Storage.RootDirectory = t.TempDir()
ctlr.Config.Storage.SubPaths = subPaths
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
"setting up mgmt routes", mgmtReadyTimeout)
defer func() {
if !found {
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
// without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
mgmtResp := extensions.StrippedConfig{}
err = json.Unmarshal(resp.Body(), &mgmtResp)
So(err, ShouldBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd, ShouldNotBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd.Path, ShouldEqual, "")
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
So(mgmtResp.HTTP.Auth.Bearer, ShouldNotBeNil)
So(mgmtResp.HTTP.Auth.Bearer.Realm, ShouldEqual, "realm")
So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, "service")
So(mgmtResp.HTTP.Auth.APIKey, ShouldBeFalse)
})
Convey("Verify mgmt auth info route enabled with bearer", t, func() {
conf.HTTP.Auth.HTPasswd.Path = ""
conf.HTTP.Auth.LDAP = nil
conf.HTTP.Auth.Bearer = &config.BearerConfig{
Realm: "realm",
Service: "service",
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
"setting up mgmt routes", mgmtReadyTimeout)
defer func() {
if !found {
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
// without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
mgmtResp := extensions.StrippedConfig{}
err = json.Unmarshal(resp.Body(), &mgmtResp)
So(err, ShouldBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd, ShouldBeNil)
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
So(mgmtResp.HTTP.Auth.Bearer, ShouldNotBeNil)
So(mgmtResp.HTTP.Auth.Bearer.Realm, ShouldEqual, "realm")
So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, "service")
So(mgmtResp.HTTP.Auth.APIKey, ShouldBeFalse)
})
Convey("Verify mgmt auth info route enabled with openID", t, func() {
conf.HTTP.Auth.HTPasswd.Path = ""
conf.HTTP.Auth.LDAP = nil
conf.HTTP.Auth.Bearer = nil
openIDProviders := make(map[string]config.OpenIDProviderConfig)
openIDProviders["oidc"] = config.OpenIDProviderConfig{
ClientID: mockOIDCConfig.ClientID,
ClientSecret: mockOIDCConfig.ClientSecret,
Issuer: mockOIDCConfig.Issuer,
}
conf.HTTP.Auth.OpenID = &config.OpenIDConfig{
Providers: openIDProviders,
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
"setting up mgmt routes", mgmtReadyTimeout)
defer func() {
if !found {
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
// without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
mgmtResp := extensions.StrippedConfig{}
err = json.Unmarshal(resp.Body(), &mgmtResp)
t.Logf("resp: %v", mgmtResp.HTTP.Auth.OpenID)
So(err, ShouldBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd, ShouldBeNil)
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil)
So(mgmtResp.HTTP.Auth.OpenID, ShouldNotBeNil)
So(mgmtResp.HTTP.Auth.OpenID.Providers, ShouldNotBeEmpty)
So(mgmtResp.HTTP.Auth.APIKey, ShouldBeFalse)
})
Convey("Verify mgmt auth info route enabled with empty openID provider list", t, func() {
username, seedUser := test.GenerateRandomString()
password, seedPass := test.GenerateRandomString()
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
defer func() {
conf.HTTP.Auth.HTPasswd.Path = ""
os.Remove(htpasswdPath)
}()
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
conf.HTTP.Auth.LDAP = nil
conf.HTTP.Auth.Bearer = nil
openIDProviders := make(map[string]config.OpenIDProviderConfig)
conf.HTTP.Auth.OpenID = &config.OpenIDConfig{
Providers: openIDProviders,
}
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
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()
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
"setting up mgmt routes", mgmtReadyTimeout)
defer func() {
if !found {
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
// without credentials
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
mgmtResp := extensions.StrippedConfig{}
err = json.Unmarshal(resp.Body(), &mgmtResp)
t.Logf("resp: %v", mgmtResp.HTTP.Auth.OpenID)
So(err, ShouldBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd, ShouldNotBeNil)
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil)
So(mgmtResp.HTTP.Auth.OpenID, ShouldBeNil)
So(mgmtResp.HTTP.Auth.APIKey, ShouldBeFalse)
})
Convey("Verify mgmt auth info route enabled without any auth", t, func() {
globalDir := t.TempDir()
conf := config.New()
port := test.GetFreePort()
conf.HTTP.Port = port
baseURL := test.GetBaseURL(port)
logFile, err := os.CreateTemp(globalDir, "zot-log*.txt")
So(err, ShouldBeNil)
defaultValue := true
conf.Commit = "v1.0.0"
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Log.Output = logFile.Name()
defer os.Remove(logFile.Name()) // cleanup
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
ctlrManager := test.NewControllerManager(ctlr)
ctlrManager.StartAndWait(port)
defer ctlrManager.StopServer()
resp, err := resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
mgmtResp := extensions.StrippedConfig{}
err = json.Unmarshal(resp.Body(), &mgmtResp)
So(err, ShouldBeNil)
So(mgmtResp.DistSpecVersion, ShouldResemble, conf.DistSpecVersion)
So(mgmtResp.HTTP.Auth.Bearer, ShouldBeNil)
So(mgmtResp.HTTP.Auth.HTPasswd, ShouldBeNil)
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
So(mgmtResp.HTTP.Auth.APIKey, ShouldBeFalse)
found, err := test.ReadLogFileAndSearchString(logFile.Name(),
"setting up mgmt routes", mgmtReadyTimeout)
defer func() {
if !found {
data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
t.Log(string(data))
}
}()
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
found, err = test.ReadLogFileAndSearchString(logFile.Name(),
"finished setting up mgmt routes", mgmtReadyTimeout)
So(found, ShouldBeTrue)
So(err, ShouldBeNil)
})
}
func TestMgmtWithBearer(t *testing.T) {
testCases := []struct {
name string
useLegacyAuthTestServer bool
}{
{
name: "new authentication test server",
useLegacyAuthTestServer: false,
},
{
name: "legacy authentication test server",
useLegacyAuthTestServer: true,
},
}
for _, testCase := range testCases {
Convey("Make a new controller with "+testCase.name, t, func() {
authorizedNamespace := "allowedrepo"
unauthorizedNamespace := "notallowedrepo"
var authTestServer *httptest.Server
if testCase.useLegacyAuthTestServer {
authTestServer = authutils.MakeAuthTestServerLegacy(ServerKey, unauthorizedNamespace)
} else {
authTestServer = authutils.MakeAuthTestServer(ServerKey, "RS256", 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,
},
}
defaultValue := true
conf.Extensions = &extconf.ExtensionConfig{}
conf.Extensions.Search = &extconf.SearchConfig{}
conf.Extensions.Search.Enable = &defaultValue
conf.Extensions.Search.CVE = nil
conf.Extensions.UI = &extconf.UIConfig{}
conf.Extensions.UI.Enable = &defaultValue
conf.Storage.RootDirectory = t.TempDir()
ctlr := api.NewController(conf)
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)
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)
resp, err = resty.R().SetHeader("Authorization",
"Bearer "+goodToken.AccessToken).Options(baseURL + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
resp, err = resty.R().Post(baseURL + "/v2/" + authorizedNamespace + "/blobs/uploads/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
authorizationHeader = 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/" + authorizedNamespace + "/blobs/uploads/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
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)
// test mgmt route
resp, err = resty.R().Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
mgmtResp := extensions.StrippedConfig{}
err = json.Unmarshal(resp.Body(), &mgmtResp)
So(err, ShouldBeNil)
So(mgmtResp.DistSpecVersion, ShouldResemble, conf.DistSpecVersion)
So(mgmtResp.HTTP.Auth.Bearer, ShouldNotBeNil)
So(mgmtResp.HTTP.Auth.Bearer.Realm, ShouldEqual, conf.HTTP.Auth.Bearer.Realm)
So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, conf.HTTP.Auth.Bearer.Service)
So(mgmtResp.HTTP.Auth.HTPasswd, ShouldBeNil)
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
So(mgmtResp.HTTP.Auth.APIKey, ShouldBeFalse)
resp, err = resty.R().SetBasicAuth("", "").Get(baseURL + constants.FullMgmt)
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
mgmtResp = extensions.StrippedConfig{}
err = json.Unmarshal(resp.Body(), &mgmtResp)
So(err, ShouldBeNil)
So(mgmtResp.DistSpecVersion, ShouldResemble, conf.DistSpecVersion)
So(mgmtResp.HTTP.Auth.Bearer, ShouldNotBeNil)
So(mgmtResp.HTTP.Auth.Bearer.Realm, ShouldEqual, conf.HTTP.Auth.Bearer.Realm)
So(mgmtResp.HTTP.Auth.Bearer.Service, ShouldEqual, conf.HTTP.Auth.Bearer.Service)
So(mgmtResp.HTTP.Auth.HTPasswd, ShouldBeNil)
So(mgmtResp.HTTP.Auth.LDAP, ShouldBeNil)
So(mgmtResp.HTTP.Auth.APIKey, ShouldBeFalse)
})
}
}
func TestAllowedMethodsHeaderMgmt(t *testing.T) {
defaultVal := true
Convey("Test http options response", t, func() {
conf := config.New()
port := test.GetFreePort()
conf.HTTP.Port = port
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
baseURL := test.GetBaseURL(port)
ctlr := api.NewController(conf)
ctlr.Config.Storage.RootDirectory = t.TempDir()
ctrlManager := test.NewControllerManager(ctlr)
ctrlManager.StartAndWait(port)
defer ctrlManager.StopServer()
resp, _ := resty.R().Options(baseURL + constants.FullMgmt)
So(resp, ShouldNotBeNil)
So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
})
}