mirror of
https://github.com/project-zot/zot.git
synced 2025-01-20 22:52:51 -05:00
feat(mgmt): added mgmt extension which returns current zot configuration (#1198)
Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
This commit is contained in:
parent
4c156234cb
commit
f04e66a5e2
21 changed files with 807 additions and 33 deletions
2
.github/workflows/golangci-lint.yaml
vendored
2
.github/workflows/golangci-lint.yaml
vendored
|
@ -31,7 +31,7 @@ jobs:
|
|||
|
||||
# Optional: golangci-lint command line arguments.
|
||||
# args: --issues-exit-code=0
|
||||
args: --config ./golangcilint.yaml --enable-all --build-tags debug,needprivileges,sync,scrub,search,metrics,containers_image_openpgp,lint ./cmd/... ./pkg/...
|
||||
args: --config ./golangcilint.yaml --enable-all --build-tags debug,needprivileges,sync,scrub,search,metrics,containers_image_openpgp,lint,mgmt ./cmd/... ./pkg/...
|
||||
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
# only-new-issues: true
|
||||
|
|
2
Makefile
2
Makefile
|
@ -28,7 +28,7 @@ TESTDATA := $(TOP_LEVEL)/test/data
|
|||
OS ?= linux
|
||||
ARCH ?= amd64
|
||||
BENCH_OUTPUT ?= stdout
|
||||
EXTENSIONS ?= sync,search,scrub,metrics,lint,ui
|
||||
EXTENSIONS ?= sync,search,scrub,metrics,lint,ui,mgmt
|
||||
comma:= ,
|
||||
hyphen:= -
|
||||
extended-name:=
|
||||
|
|
|
@ -48,6 +48,9 @@
|
|||
"scrub": {
|
||||
"enable": true,
|
||||
"interval": "24h"
|
||||
},
|
||||
"mgmt": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
|
||||
"zotregistry.io/zot/errors"
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
localCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
)
|
||||
|
||||
|
@ -186,12 +187,17 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
return
|
||||
}
|
||||
|
||||
if request.Header.Get("Authorization") == "" && anonymousPolicyExists(ctlr.Config.HTTP.AccessControl) {
|
||||
// Process request
|
||||
ctx := getReqContextWithAuthorization("", []string{}, request)
|
||||
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
|
||||
// we want to bypass auth for mgmt route
|
||||
isMgmtRequested := request.RequestURI == constants.FullMgmtPrefix
|
||||
|
||||
return
|
||||
if request.Header.Get("Authorization") == "" {
|
||||
if anonymousPolicyExists(ctlr.Config.HTTP.AccessControl) || isMgmtRequested {
|
||||
// Process request
|
||||
ctx := getReqContextWithAuthorization("", []string{}, request)
|
||||
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
username, passphrase, err := getUsernamePasswordBasicAuth(request)
|
||||
|
@ -204,12 +210,14 @@ func basicAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
|
||||
// some client tools might send Authorization: Basic Og== (decoded into ":")
|
||||
// empty username and password
|
||||
if username == "" && passphrase == "" && anonymousPolicyExists(ctlr.Config.HTTP.AccessControl) {
|
||||
// Process request
|
||||
ctx := getReqContextWithAuthorization("", []string{}, request)
|
||||
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
|
||||
if username == "" && passphrase == "" {
|
||||
if anonymousPolicyExists(ctlr.Config.HTTP.AccessControl) || isMgmtRequested {
|
||||
// Process request
|
||||
ctx := getReqContextWithAuthorization("", []string{}, request)
|
||||
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
|
||||
|
||||
return
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// first, HTTPPassword authN (which is local)
|
||||
|
|
|
@ -240,9 +240,13 @@ func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
return
|
||||
}
|
||||
|
||||
/* we want to bypass auth/authz for mgmt in case of authFail() authzFail()
|
||||
unauthenticated users should have access to this route, but we also need to know if the user is an admin
|
||||
*/
|
||||
isMgmtRequested := request.RequestURI == constants.FullMgmtPrefix
|
||||
|
||||
acCtrlr := NewAccessController(ctlr.Config)
|
||||
|
||||
// allow anonymous authz if no authn present and only default policies are present
|
||||
var identity string
|
||||
var err error
|
||||
|
||||
|
@ -264,6 +268,7 @@ func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
for _, cert := range request.TLS.PeerCertificates {
|
||||
identity = cert.Subject.CommonName
|
||||
}
|
||||
|
||||
// if we still don't have an identity
|
||||
if identity == "" {
|
||||
acCtrlr.Log.Info().Msg("couldn't get identity from TLS certificate")
|
||||
|
@ -274,14 +279,10 @@ func AuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
|
||||
ctx := acCtrlr.getContext(identity, request)
|
||||
|
||||
// will return only repos on which client is authorized to read
|
||||
if request.RequestURI == fmt.Sprintf("%s%s", constants.RoutePrefix, constants.ExtCatalogPrefix) {
|
||||
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(request.RequestURI, constants.FullSearchPrefix) {
|
||||
// for extensions we only need to know if the user is admin and what repos he can read, so run next()
|
||||
if request.RequestURI == fmt.Sprintf("%s%s", constants.RoutePrefix, constants.ExtCatalogPrefix) ||
|
||||
strings.Contains(request.RequestURI, constants.FullSearchPrefix) ||
|
||||
isMgmtRequested {
|
||||
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
|
||||
|
||||
return
|
||||
|
|
|
@ -69,7 +69,7 @@ type HTTPConfig struct {
|
|||
AllowOrigin string // comma separated
|
||||
TLS *TLSConfig
|
||||
Auth *AuthConfig
|
||||
AccessControl *AccessControlConfig
|
||||
AccessControl *AccessControlConfig `mapstructure:"accessControl,omitempty"`
|
||||
Realm string
|
||||
Ratelimit *RatelimitConfig `mapstructure:",omitempty"`
|
||||
}
|
||||
|
|
|
@ -7,4 +7,6 @@ const (
|
|||
// zot specific extensions.
|
||||
ExtSearchPrefix = "/_zot/ext/search"
|
||||
FullSearchPrefix = RoutePrefix + ExtSearchPrefix
|
||||
ExtMgmtPrefix = "/_zot/ext/mgmt"
|
||||
FullMgmtPrefix = RoutePrefix + ExtMgmtPrefix
|
||||
)
|
||||
|
|
|
@ -3138,6 +3138,12 @@ func TestAuthorizationWithMultiplePolicies(t *testing.T) {
|
|||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
// with empty username:password
|
||||
resp, err = resty.R().SetHeader("Authorization", "Basic Og==").Get(baseURL + "/v2/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
||||
|
||||
// add "test" user to global policy with create permission
|
||||
repoPolicy.Policies[0].Users = append(repoPolicy.Policies[0].Users, "test")
|
||||
repoPolicy.Policies[0].Actions = append(repoPolicy.Policies[0].Actions, "create")
|
||||
|
@ -7290,6 +7296,60 @@ func TestDistSpecExtensions(t *testing.T) {
|
|||
So(extensionList.Extensions[0].Endpoints[0], ShouldEqual, constants.FullSearchPrefix)
|
||||
})
|
||||
|
||||
Convey("start zot server with search and mgmt extensions", t, func(c C) {
|
||||
conf := config.New()
|
||||
port := test.GetFreePort()
|
||||
baseURL := test.GetBaseURL(port)
|
||||
|
||||
conf.HTTP.Port = port
|
||||
|
||||
defaultVal := true
|
||||
|
||||
searchConfig := &extconf.SearchConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
}
|
||||
|
||||
mgmtConfg := &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
|
||||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Search: searchConfig,
|
||||
Mgmt: mgmtConfg,
|
||||
}
|
||||
|
||||
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, 2)
|
||||
So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 1)
|
||||
So(len(extensionList.Extensions[1].Endpoints), ShouldEqual, 1)
|
||||
So(extensionList.Extensions[0].Name, ShouldEqual, "_zot")
|
||||
So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md")
|
||||
So(extensionList.Extensions[0].Description, ShouldNotBeEmpty)
|
||||
So(extensionList.Extensions[0].Endpoints[0], ShouldEqual, constants.FullSearchPrefix)
|
||||
So(extensionList.Extensions[1].Name, ShouldEqual, "_zot")
|
||||
So(extensionList.Extensions[1].URL, ShouldContainSubstring, "_zot.md")
|
||||
So(extensionList.Extensions[1].Description, ShouldNotBeEmpty)
|
||||
So(extensionList.Extensions[1].Endpoints[0], ShouldEqual, constants.FullMgmtPrefix)
|
||||
})
|
||||
|
||||
Convey("start minimal zot server", t, func(c C) {
|
||||
conf := config.New()
|
||||
port := test.GetFreePort()
|
||||
|
|
|
@ -128,6 +128,7 @@ func (rh *RouteHandler) SetupRoutes() {
|
|||
ext.SetupMetricsRoutes(rh.c.Config, rh.c.Router, rh.c.StoreController, AuthHandler(rh.c), rh.c.Log)
|
||||
ext.SetupSearchRoutes(rh.c.Config, prefixedRouter, rh.c.StoreController, rh.c.RepoDB, rh.c.CveInfo, rh.c.Log)
|
||||
ext.SetupUIRoutes(rh.c.Config, rh.c.Router, rh.c.StoreController, rh.c.Log)
|
||||
ext.SetupMgmtRoutes(rh.c.Config, prefixedRouter, rh.c.Log)
|
||||
gqlPlayground.SetupGQLPlaygroundRoutes(rh.c.Config, prefixedRouter, rh.c.StoreController, rh.c.Log)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -108,7 +108,7 @@ func TestServeExtensions(t *testing.T) {
|
|||
data, err := os.ReadFile(logFile.Name())
|
||||
So(err, ShouldBeNil)
|
||||
So(string(data), ShouldContainSubstring,
|
||||
"\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":null,\"UI\":null") //nolint:lll // gofumpt conflicts with lll
|
||||
"\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":null,\"UI\":null,\"Mgmt\":null") //nolint:lll // gofumpt conflicts with lll
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -776,6 +776,67 @@ func TestServeSearchDisabled(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestServeMgmtExtension(t *testing.T) {
|
||||
oldArgs := os.Args
|
||||
|
||||
defer func() { os.Args = oldArgs }()
|
||||
|
||||
Convey("Mgmt implicitly enabled", t, func(c C) {
|
||||
content := `{
|
||||
"storage": {
|
||||
"rootDirectory": "%s"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "%s"
|
||||
},
|
||||
"log": {
|
||||
"level": "debug",
|
||||
"output": "%s"
|
||||
},
|
||||
"extensions": {
|
||||
"Mgmt": {
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
logPath, err := runCLIWithConfig(t.TempDir(), content)
|
||||
So(err, ShouldBeNil)
|
||||
data, err := os.ReadFile(logPath)
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(logPath) // clean up
|
||||
So(string(data), ShouldContainSubstring, "\"Mgmt\":{\"Enable\":true}")
|
||||
})
|
||||
|
||||
Convey("Mgmt disabled", t, func(c C) {
|
||||
content := `{
|
||||
"storage": {
|
||||
"rootDirectory": "%s"
|
||||
},
|
||||
"http": {
|
||||
"address": "127.0.0.1",
|
||||
"port": "%s"
|
||||
},
|
||||
"log": {
|
||||
"level": "debug",
|
||||
"output": "%s"
|
||||
},
|
||||
"extensions": {
|
||||
"Mgmt": {
|
||||
"enable": "false"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
logPath, err := runCLIWithConfig(t.TempDir(), content)
|
||||
So(err, ShouldBeNil)
|
||||
data, err := os.ReadFile(logPath)
|
||||
So(err, ShouldBeNil)
|
||||
defer os.Remove(logPath) // clean up
|
||||
So(string(data), ShouldContainSubstring, "\"Mgmt\":{\"Enable\":false}")
|
||||
})
|
||||
}
|
||||
|
||||
func readLogFileAndSearchString(logPath string, stringToMatch string, timeout time.Duration) (bool, error) { //nolint:unparam,lll
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancelFunc()
|
||||
|
|
|
@ -461,6 +461,13 @@ func applyDefaultValues(config *config.Config, viperInstance *viper.Viper) {
|
|||
// Note: In case scrub is not empty the config.Extensions will not be nil and we will not reach here
|
||||
config.Extensions.Scrub = &extconf.ScrubConfig{}
|
||||
}
|
||||
|
||||
_, ok = extMap["mgmt"]
|
||||
if ok {
|
||||
// we found a config like `"extensions": {"mgmt:": {}}`
|
||||
// Note: In case mgmt is not empty the config.Extensions will not be nil and we will not reach here
|
||||
config.Extensions.Mgmt = &extconf.MgmtConfig{}
|
||||
}
|
||||
}
|
||||
|
||||
if config.Extensions != nil {
|
||||
|
@ -492,6 +499,12 @@ func applyDefaultValues(config *config.Config, viperInstance *viper.Viper) {
|
|||
}
|
||||
}
|
||||
|
||||
if config.Extensions.Mgmt != nil {
|
||||
if config.Extensions.Mgmt.Enable == nil {
|
||||
config.Extensions.Mgmt.Enable = &defaultVal
|
||||
}
|
||||
}
|
||||
|
||||
if config.Extensions.Scrub != nil {
|
||||
if config.Extensions.Scrub.Enable == nil {
|
||||
config.Extensions.Scrub.Enable = &defaultVal
|
||||
|
|
|
@ -616,7 +616,7 @@ func TestVerify(t *testing.T) {
|
|||
// sub paths that point to same directory should have same storage config.
|
||||
content = []byte(`{"storage":{"rootDirectory":"/tmp/zot",
|
||||
"subPaths": {"/a": {"rootDirectory": "/zot-a","dedupe":"true"},
|
||||
"/b": {{"rootDirectory": "/zot-a","dedupe":"false"}}}},
|
||||
"/b": {"rootDirectory": "/zot-a","dedupe":"false"}}},
|
||||
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
|
||||
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`)
|
||||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
|
@ -626,7 +626,7 @@ func TestVerify(t *testing.T) {
|
|||
|
||||
// sub paths that point to default root directory should not be allowed.
|
||||
content = []byte(`{"storage":{"rootDirectory":"/tmp/zot",
|
||||
"subPaths": {"/a": {"rootDirectory": "/tmp/zot","dedupe":"true"},"/b": {{"rootDirectory": "/zot-a"}}}},
|
||||
"subPaths": {"/a": {"rootDirectory": "/tmp/zot","dedupe":"true"},"/b": {"rootDirectory": "/zot-a"}}},
|
||||
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
|
||||
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`)
|
||||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
|
@ -636,7 +636,7 @@ func TestVerify(t *testing.T) {
|
|||
|
||||
content = []byte(`{"storage":{"rootDirectory":"/tmp/zot",
|
||||
"subPaths": {"/a": {"rootDirectory": "/zot-a","dedupe":"true","gc":"true"},
|
||||
"/b": {"rootDirectory": "/zot-a","dedupe":"true","gc":"false"}}}},
|
||||
"/b": {"rootDirectory": "/zot-a","dedupe":"true","gc":"false"}}},
|
||||
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
|
||||
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`)
|
||||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
|
@ -646,7 +646,7 @@ func TestVerify(t *testing.T) {
|
|||
|
||||
content = []byte(`{"storage":{"rootDirectory":"/tmp/zot",
|
||||
"subPaths": {"/a": {"rootDirectory": "/zot-a","dedupe":"true","gc":"true"},
|
||||
"/b": {"rootDirectory": "/zot-a","dedupe":"true","gc":"true","gcDelay":"1s"}}}},
|
||||
"/b": {"rootDirectory": "/zot-a","dedupe":"true","gc":"true","gcDelay":"1s"}}},
|
||||
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
|
||||
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`)
|
||||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
|
@ -656,7 +656,7 @@ func TestVerify(t *testing.T) {
|
|||
|
||||
content = []byte(`{"storage":{"rootDirectory":"/tmp/zot",
|
||||
"subPaths": {"/a": {"rootDirectory": "/zot-a","dedupe":"true","gc":"true","gcDelay":"1s","gcInterval":"1s"},
|
||||
"/b": {"rootDirectory": "/zot-a","dedupe":"true","gc":"true","gcDelay":"1s"}}}},
|
||||
"/b": {"rootDirectory": "/zot-a","dedupe":"true","gc":"true","gcDelay":"1s"}}},
|
||||
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
|
||||
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`)
|
||||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
|
@ -666,7 +666,7 @@ func TestVerify(t *testing.T) {
|
|||
|
||||
content = []byte(`{"storage":{"rootDirectory":"/tmp/zot",
|
||||
"subPaths": {"/a": {"rootDirectory": "/tmp/zot","dedupe":"true","gc":"true","gcDelay":"1s","gcInterval":"1s"},
|
||||
"/b": {"rootDirectory": "/zot-a","dedupe":"true","gc":"true","gcDelay":"1s"}}}},
|
||||
"/b": {"rootDirectory": "/zot-a","dedupe":"true","gc":"true","gcDelay":"1s"}}},
|
||||
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
|
||||
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}}}`)
|
||||
err = os.WriteFile(tmpfile.Name(), content, 0o0600)
|
||||
|
@ -741,7 +741,7 @@ func TestVerify(t *testing.T) {
|
|||
"actions":["read","create","update","delete"]
|
||||
}
|
||||
}
|
||||
}}`)
|
||||
}`)
|
||||
_, err = tmpfile.Write(content)
|
||||
So(err, ShouldBeNil)
|
||||
err = tmpfile.Close()
|
||||
|
@ -893,7 +893,7 @@ func TestVerify(t *testing.T) {
|
|||
content := []byte(`{"storage":{"rootDirectory":"/tmp/zot"},
|
||||
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
|
||||
"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1},
|
||||
"accessControl":{"[":{"policies":[],"anonymousPolicy":[]}}}}`)
|
||||
"accessControl":{"repositories":{"[":{"policies":[],"anonymousPolicy":[]}}}}}`)
|
||||
_, err = tmpfile.Write(content)
|
||||
So(err, ShouldBeNil)
|
||||
err = tmpfile.Close()
|
||||
|
|
|
@ -211,3 +211,23 @@ func DirExists(d string) bool {
|
|||
|
||||
return true
|
||||
}
|
||||
|
||||
// Used to filter a json fields by using an intermediate struct.
|
||||
func MarshalThroughStruct(obj interface{}, throughStruct interface{}) ([]byte, error) {
|
||||
toJSON, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(toJSON, throughStruct)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
toJSON, err = json.Marshal(throughStruct)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
return toJSON, nil
|
||||
}
|
||||
|
|
|
@ -24,6 +24,28 @@ func TestCommon(t *testing.T) {
|
|||
So(common.Contains([]string{}, "apple"), ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("test MarshalThroughStruct()", t, func() {
|
||||
cfg := config.New()
|
||||
|
||||
newCfg := struct {
|
||||
DistSpecVersion string
|
||||
}{}
|
||||
|
||||
_, err := common.MarshalThroughStruct(cfg, &newCfg)
|
||||
So(err, ShouldBeNil)
|
||||
So(newCfg.DistSpecVersion, ShouldEqual, cfg.DistSpecVersion)
|
||||
|
||||
// negative
|
||||
obj := make(chan int)
|
||||
toObj := config.New()
|
||||
|
||||
_, err = common.MarshalThroughStruct(obj, &toObj)
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
_, err = common.MarshalThroughStruct(toObj, &obj)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("test getTLSConfig()", t, func() {
|
||||
caCertPool, _ := x509.SystemCertPool()
|
||||
tlsConfig, err := common.GetTLSConfig("wrongPath", caCertPool)
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
Component | Endpoint | Description
|
||||
--- | --- | ---
|
||||
[`search`](search/search.md) | `/v2/_zot/ext/search` | efficient and enhanced registry search capabilities using graphQL backend
|
||||
[`mgmt`](mgmt.md) | `/v2/_zot/ext/mgmt` | config management
|
||||
|
||||
|
||||
# References
|
||||
|
|
|
@ -18,6 +18,11 @@ type ExtensionConfig struct {
|
|||
Scrub *ScrubConfig
|
||||
Lint *LintConfig
|
||||
UI *UIConfig
|
||||
Mgmt *MgmtConfig
|
||||
}
|
||||
|
||||
type MgmtConfig struct {
|
||||
BaseConfig `mapstructure:",squash"`
|
||||
}
|
||||
|
||||
type LintConfig struct {
|
||||
|
|
90
pkg/extensions/extension_mgmt.go
Normal file
90
pkg/extensions/extension_mgmt.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
//go:build mgmt
|
||||
// +build mgmt
|
||||
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
"zotregistry.io/zot/pkg/common"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
)
|
||||
|
||||
type HTPasswd struct {
|
||||
Path string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
type BearerConfig struct {
|
||||
Realm string `json:"realm,omitempty"`
|
||||
Service string `json:"service,omitempty"`
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
HTPasswd *HTPasswd `json:"htpasswd,omitempty" mapstructure:"htpasswd"`
|
||||
Bearer *BearerConfig `json:"bearer,omitempty" mapstructure:"bearer"`
|
||||
LDAP *struct {
|
||||
Address string `json:"address,omitempty" mapstructure:"address"`
|
||||
} `json:"ldap,omitempty" mapstructure:"ldap"`
|
||||
}
|
||||
|
||||
type StrippedConfig struct {
|
||||
DistSpecVersion string `json:"distSpecVersion" mapstructure:"distSpecVersion"`
|
||||
BinaryType string `json:"binaryType" mapstructure:"binaryType"`
|
||||
HTTP struct {
|
||||
Auth *Auth `json:"auth,omitempty" mapstructure:"auth"`
|
||||
} `json:"http" mapstructure:"http"`
|
||||
}
|
||||
|
||||
func (auth Auth) MarshalJSON() ([]byte, error) {
|
||||
type localAuth Auth
|
||||
|
||||
if auth.Bearer == nil && auth.LDAP == nil &&
|
||||
auth.HTPasswd.Path == "" {
|
||||
auth.HTPasswd = nil
|
||||
|
||||
return json.Marshal((localAuth)(auth))
|
||||
}
|
||||
|
||||
if auth.HTPasswd.Path == "" && auth.LDAP == nil {
|
||||
auth.HTPasswd = nil
|
||||
} else {
|
||||
auth.HTPasswd.Path = ""
|
||||
}
|
||||
|
||||
auth.LDAP = nil
|
||||
|
||||
return json.Marshal((localAuth)(auth))
|
||||
}
|
||||
|
||||
type mgmt struct {
|
||||
config *config.Config
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (mgmt *mgmt) handler(response http.ResponseWriter, request *http.Request) {
|
||||
sanitizedConfig := mgmt.config.Sanitize()
|
||||
|
||||
buf, err := common.MarshalThroughStruct(sanitizedConfig, &StrippedConfig{})
|
||||
if err != nil {
|
||||
mgmt.log.Error().Err(err).Msg("mgmt: couldn't marshal config response")
|
||||
|
||||
response.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
_, _ = response.Write(buf)
|
||||
}
|
||||
|
||||
func SetupMgmtRoutes(config *config.Config, router *mux.Router, log log.Logger) {
|
||||
if config.Extensions.Mgmt != nil && *config.Extensions.Mgmt.Enable {
|
||||
log.Info().Msg("setting up mgmt routes")
|
||||
|
||||
mgmt := mgmt{config: config, log: log}
|
||||
|
||||
router.PathPrefix(constants.ExtMgmtPrefix).Methods("GET").HandlerFunc(mgmt.handler)
|
||||
}
|
||||
}
|
16
pkg/extensions/extension_mgmt_disabled.go
Normal file
16
pkg/extensions/extension_mgmt_disabled.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
//go:build !mgmt
|
||||
// +build !mgmt
|
||||
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
)
|
||||
|
||||
func SetupMgmtRoutes(config *config.Config, router *mux.Router, log log.Logger) {
|
||||
log.Warn().Msg("skipping setting up mgmt routes because given zot binary doesn't include this feature," +
|
||||
"please build a binary that does so")
|
||||
}
|
|
@ -197,6 +197,16 @@ func GetExtensions(config *config.Config) distext.ExtensionList {
|
|||
extensions = append(extensions, searchExt)
|
||||
}
|
||||
|
||||
if config.Extensions != nil && config.Extensions.Mgmt != nil {
|
||||
endpoints := []string{constants.FullMgmtPrefix}
|
||||
mgmtExt := getExtension("_zot",
|
||||
"https://github.com/project-zot/zot/blob/"+config.ReleaseTag+"/pkg/extensions/_zot.md",
|
||||
"zot registry extensions",
|
||||
endpoints)
|
||||
|
||||
extensions = append(extensions, mgmtExt)
|
||||
}
|
||||
|
||||
extensionList.Extensions = extensions
|
||||
|
||||
return extensionList
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
//go:build sync || metrics
|
||||
// +build sync metrics
|
||||
//go:build sync || metrics || mgmt
|
||||
// +build sync metrics mgmt
|
||||
|
||||
package extensions_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/resty.v1"
|
||||
|
||||
"zotregistry.io/zot/pkg/api"
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
"zotregistry.io/zot/pkg/extensions"
|
||||
extconf "zotregistry.io/zot/pkg/extensions/config"
|
||||
syncconf "zotregistry.io/zot/pkg/extensions/config/sync"
|
||||
"zotregistry.io/zot/pkg/test"
|
||||
|
@ -92,3 +97,415 @@ func TestMetricsExtension(t *testing.T) {
|
|||
"Prometheus instrumentation Path not set, changing to '/metrics'.")
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
defaultValue := true
|
||||
|
||||
Convey("Verify mgmt route enabled with htpasswd", t, func() {
|
||||
htpasswdPath := test.MakeHtpasswdFile()
|
||||
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
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{}
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = globalDir
|
||||
ctlr.Config.Storage.SubPaths = subPaths
|
||||
|
||||
ctlrManager := test.NewControllerManager(ctlr)
|
||||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
data, _ := os.ReadFile(logFile.Name())
|
||||
|
||||
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
|
||||
|
||||
// without credentials
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
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)
|
||||
|
||||
// with credentials
|
||||
resp, err = resty.R().SetBasicAuth("test", "test").Get(baseURL + constants.FullMgmtPrefix)
|
||||
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)
|
||||
|
||||
// with wrong credentials
|
||||
resp, err = resty.R().SetBasicAuth("test", "wrong").Get(baseURL + constants.FullMgmtPrefix)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
Convey("Verify mgmt route enabled with ldap", t, func() {
|
||||
conf.HTTP.Auth.LDAP = &config.LDAPConfig{
|
||||
BindDN: "binddn",
|
||||
BaseDN: "basedn",
|
||||
Address: "ldapexample",
|
||||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
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{}
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = globalDir
|
||||
ctlr.Config.Storage.SubPaths = subPaths
|
||||
|
||||
ctlrManager := test.NewControllerManager(ctlr)
|
||||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
data, _ := os.ReadFile(logFile.Name())
|
||||
|
||||
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
|
||||
|
||||
// without credentials
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
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)
|
||||
})
|
||||
|
||||
Convey("Verify mgmt route enabled with htpasswd + ldap", t, func() {
|
||||
htpasswdPath := test.MakeHtpasswdFile()
|
||||
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
|
||||
conf.HTTP.Auth.LDAP = &config.LDAPConfig{
|
||||
BindDN: "binddn",
|
||||
BaseDN: "basedn",
|
||||
Address: "ldapexample",
|
||||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
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{}
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = globalDir
|
||||
ctlr.Config.Storage.SubPaths = subPaths
|
||||
|
||||
ctlrManager := test.NewControllerManager(ctlr)
|
||||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
data, _ := os.ReadFile(logFile.Name())
|
||||
|
||||
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
|
||||
|
||||
// without credentials
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
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)
|
||||
|
||||
// with credentials
|
||||
resp, err = resty.R().SetBasicAuth("test", "test").Get(baseURL + constants.FullMgmtPrefix)
|
||||
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)
|
||||
})
|
||||
|
||||
Convey("Verify mgmt route enabled with htpasswd + ldap + bearer", t, func() {
|
||||
htpasswdPath := test.MakeHtpasswdFile()
|
||||
conf.HTTP.Auth.HTPasswd.Path = htpasswdPath
|
||||
conf.HTTP.Auth.LDAP = &config.LDAPConfig{
|
||||
BindDN: "binddn",
|
||||
BaseDN: "basedn",
|
||||
Address: "ldapexample",
|
||||
}
|
||||
|
||||
conf.HTTP.Auth.Bearer = &config.BearerConfig{
|
||||
Realm: "realm",
|
||||
Service: "service",
|
||||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
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{}
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = globalDir
|
||||
ctlr.Config.Storage.SubPaths = subPaths
|
||||
|
||||
ctlrManager := test.NewControllerManager(ctlr)
|
||||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
data, _ := os.ReadFile(logFile.Name())
|
||||
|
||||
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
|
||||
|
||||
// without credentials
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
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")
|
||||
|
||||
// with credentials
|
||||
resp, err = resty.R().SetBasicAuth("test", "test").Get(baseURL + constants.FullMgmtPrefix)
|
||||
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")
|
||||
})
|
||||
|
||||
Convey("Verify mgmt route enabled with ldap + bearer", t, func() {
|
||||
conf.HTTP.Auth.HTPasswd.Path = ""
|
||||
conf.HTTP.Auth.LDAP = &config.LDAPConfig{
|
||||
BindDN: "binddn",
|
||||
BaseDN: "basedn",
|
||||
Address: "ldapexample",
|
||||
}
|
||||
|
||||
conf.HTTP.Auth.Bearer = &config.BearerConfig{
|
||||
Realm: "realm",
|
||||
Service: "service",
|
||||
}
|
||||
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
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{}
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = globalDir
|
||||
ctlr.Config.Storage.SubPaths = subPaths
|
||||
|
||||
ctlrManager := test.NewControllerManager(ctlr)
|
||||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
data, _ := os.ReadFile(logFile.Name())
|
||||
|
||||
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
|
||||
|
||||
// without credentials
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
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")
|
||||
})
|
||||
|
||||
Convey("Verify mgmt 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.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
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{}
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = globalDir
|
||||
ctlr.Config.Storage.SubPaths = subPaths
|
||||
|
||||
ctlrManager := test.NewControllerManager(ctlr)
|
||||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
data, _ := os.ReadFile(logFile.Name())
|
||||
|
||||
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
|
||||
|
||||
// without credentials
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
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")
|
||||
})
|
||||
|
||||
Convey("Verify mgmt 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.Mgmt = &extconf.MgmtConfig{
|
||||
BaseConfig: extconf.BaseConfig{
|
||||
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{}
|
||||
|
||||
ctlr.Config.Storage.RootDirectory = globalDir
|
||||
ctlr.Config.Storage.SubPaths = subPaths
|
||||
|
||||
ctlrManager := test.NewControllerManager(ctlr)
|
||||
ctlrManager.StartAndWait(port)
|
||||
defer ctlrManager.StopServer()
|
||||
|
||||
resp, err := resty.R().Get(baseURL + constants.FullMgmtPrefix)
|
||||
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)
|
||||
|
||||
data, _ := os.ReadFile(logFile.Name())
|
||||
So(string(data), ShouldContainSubstring, "setting up mgmt routes")
|
||||
})
|
||||
}
|
||||
|
|
44
pkg/extensions/mgmt.md
Normal file
44
pkg/extensions/mgmt.md
Normal file
|
@ -0,0 +1,44 @@
|
|||
# `mgmt`
|
||||
|
||||
`mgmt` component provides an endpoint for configuration management
|
||||
|
||||
Response depends on the user privileges:
|
||||
- unauthenticated and authenticated users will get a stripped config
|
||||
- admins will get full configuration with passwords hidden (not implemented yet)
|
||||
|
||||
|
||||
| Supported queries | Input | Output | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| [Get current configuration](#get-current-configuration) | None | config json | Get current zot configuration |
|
||||
|
||||
|
||||
## Get current configuration
|
||||
|
||||
**Sample request**
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/v2/_zot/ext/mgmt | jq
|
||||
```
|
||||
|
||||
**Sample response**
|
||||
|
||||
```json
|
||||
{
|
||||
"distSpecVersion": "1.1.0-dev",
|
||||
"binaryType": "-sync-search-scrub-metrics-lint-ui-mgmt",
|
||||
"http": {
|
||||
"auth": {
|
||||
"htpasswd": {},
|
||||
"bearer": {
|
||||
"realm": "https://auth.myreg.io/auth/token",
|
||||
"service": "myauth"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If ldap or htpasswd are enabled mgmt will return `{"htpasswd": {}}` indicating that clients can authenticate with basic auth credentials.
|
||||
|
||||
If any key is present under `'auth'` key, in the mgmt response, it means that particular authentication method is enabled.
|
||||
|
Loading…
Add table
Reference in a new issue