From 7b1441d5ff002fc79ac94c5014725c72c2417a71 Mon Sep 17 00:00:00 2001 From: Alexei Dodon Date: Thu, 5 Oct 2023 23:47:32 +0300 Subject: [PATCH] fix: metrics should be protected behind authZ Signed-off-by: Alexei Dodon --- examples/config-metrics-authz.json | 39 +++ pkg/api/authn.go | 4 +- pkg/api/authz.go | 61 +++- pkg/api/config/config.go | 5 + pkg/api/routes.go | 2 +- pkg/extensions/README.md | 6 +- pkg/extensions/extension_metrics.go | 5 +- pkg/extensions/extension_metrics_disabled.go | 5 +- pkg/extensions/monitoring/monitoring_test.go | 322 +++++++++++++++++++ pkg/test/skip/skip_test.go | 37 +++ test/blackbox/helpers_metrics.bash | 3 + test/blackbox/metrics.bats | 24 +- test/blackbox/metrics_minimal.bats | 26 +- 13 files changed, 508 insertions(+), 31 deletions(-) create mode 100644 examples/config-metrics-authz.json create mode 100644 pkg/test/skip/skip_test.go diff --git a/examples/config-metrics-authz.json b/examples/config-metrics-authz.json new file mode 100644 index 000000000..efeb8ca2f --- /dev/null +++ b/examples/config-metrics-authz.json @@ -0,0 +1,39 @@ +{ + "distSpecVersion": "1.1.0-dev", + "storage": { + "rootDirectory": "/tmp/zot" + }, + "http": { + "address": "127.0.0.1", + "port": "8080", + "auth": { + "htpasswd": { + "path": "test/data/htpasswd" + } + }, + "accessControl": { + "metrics":{ + "users": ["metrics"] + }, + "repositories": { + "**": { + "anonymousPolicy": [ + "read" + ], + "defaultPolicy": ["read","create"] + } + } + } + }, + "log": { + "level": "debug" + }, + "extensions": { + "metrics": { + "enable": true, + "prometheus": { + "path": "/metrics" + } + } + } +} diff --git a/pkg/api/authn.go b/pkg/api/authn.go index bd7f03d73..9777b8bee 100644 --- a/pkg/api/authn.go +++ b/pkg/api/authn.go @@ -57,7 +57,7 @@ func AuthHandler(ctlr *Controller) mux.MiddlewareFunc { return bearerAuthHandler(ctlr) } - return authnMiddleware.TryAuthnHandlers(ctlr) + return authnMiddleware.tryAuthnHandlers(ctlr) } func (amw *AuthnMiddleware) sessionAuthn(ctlr *Controller, userAc *reqCtx.UserAccessControl, @@ -247,7 +247,7 @@ func (amw *AuthnMiddleware) basicAuthn(ctlr *Controller, userAc *reqCtx.UserAcce return false, nil } -func (amw *AuthnMiddleware) TryAuthnHandlers(ctlr *Controller) mux.MiddlewareFunc { //nolint: gocyclo +func (amw *AuthnMiddleware) tryAuthnHandlers(ctlr *Controller) mux.MiddlewareFunc { //nolint: gocyclo // no password based authN, if neither LDAP nor HTTP BASIC is enabled if !ctlr.Config.IsBasicAuthnEnabled() { return noPasswdAuth(ctlr) diff --git a/pkg/api/authz.go b/pkg/api/authz.go index 3228e8b03..42c972478 100644 --- a/pkg/api/authz.go +++ b/pkg/api/authz.go @@ -191,14 +191,10 @@ func (ac *AccessController) getAuthnMiddlewareContext(authnType string, request func (ac *AccessController) isPermitted(userGroups []string, username, action string, policyGroup config.PolicyGroup, ) bool { - var result bool - // check repo/system based policies for _, p := range policyGroup.Policies { if common.Contains(p.Users, username) && common.Contains(p.Actions, action) { - result = true - - return result + return true } } @@ -207,9 +203,7 @@ func (ac *AccessController) isPermitted(userGroups []string, username, action st if common.Contains(p.Actions, action) { for _, group := range p.Groups { if common.Contains(userGroups, group) { - result = true - - return result + return true } } } @@ -217,20 +211,16 @@ func (ac *AccessController) isPermitted(userGroups []string, username, action st } // check defaultPolicy - if !result { - if common.Contains(policyGroup.DefaultPolicy, action) && username != "" { - result = true - } + if common.Contains(policyGroup.DefaultPolicy, action) && username != "" { + return true } // check anonymousPolicy - if !result { - if common.Contains(policyGroup.AnonymousPolicy, action) && username == "" { - result = true - } + if common.Contains(policyGroup.AnonymousPolicy, action) && username == "" { + return true } - return result + return false } func BaseAuthzHandler(ctlr *Controller) mux.MiddlewareFunc { @@ -343,3 +333,40 @@ func DistSpecAuthzHandler(ctlr *Controller) mux.MiddlewareFunc { }) } } + +func MetricsAuthzHandler(ctlr *Controller) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { + if ctlr.Config.HTTP.AccessControl == nil { + // allow access to authenticated user as anonymous policy does not exist + next.ServeHTTP(response, request) + + return + } + if len(ctlr.Config.HTTP.AccessControl.Metrics.Users) == 0 { + log := ctlr.Log + log.Warn().Msg("auth is enabled but no metrics users in accessControl: /metrics is unaccesible") + common.AuthzFail(response, request, "", ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay) + + return + } + + // get access control context made in authn.go + userAc, err := reqCtx.UserAcFromContext(request.Context()) + if err != nil { // should never happen + common.AuthzFail(response, request, "", ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay) + + return + } + + username := userAc.GetUsername() + if !common.Contains(ctlr.Config.HTTP.AccessControl.Metrics.Users, username) { + common.AuthzFail(response, request, username, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay) + + return + } + + next.ServeHTTP(response, request) //nolint:contextcheck + }) + } +} diff --git a/pkg/api/config/config.go b/pkg/api/config/config.go index 8ab156e32..c6ed53da2 100644 --- a/pkg/api/config/config.go +++ b/pkg/api/config/config.go @@ -131,6 +131,7 @@ type AccessControlConfig struct { Repositories Repositories `json:"repositories" mapstructure:"repositories"` AdminPolicy Policy Groups Groups + Metrics Metrics } func (config *AccessControlConfig) AnonymousPolicyExists() bool { @@ -168,6 +169,10 @@ type Policy struct { Groups []string } +type Metrics struct { + Users []string +} + type Config struct { DistSpecVersion string `json:"distSpecVersion" mapstructure:"distSpecVersion"` GoVersion string diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 1db9a8044..6cd5d9a73 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -183,7 +183,7 @@ func (rh *RouteHandler) SetupRoutes() { pprof.SetupPprofRoutes(rh.c.Config, prefixedRouter, authHandler, rh.c.Log) // Preconditions for enabling the actual extension routes are part of extensions themselves - ext.SetupMetricsRoutes(rh.c.Config, rh.c.Router, authHandler, rh.c.Log, rh.c.Metrics) + ext.SetupMetricsRoutes(rh.c.Config, rh.c.Router, authHandler, MetricsAuthzHandler(rh.c), rh.c.Log, rh.c.Metrics) ext.SetupSearchRoutes(rh.c.Config, prefixedRouter, rh.c.StoreController, rh.c.MetaDB, rh.c.CveScanner, rh.c.Log) ext.SetupImageTrustRoutes(rh.c.Config, prefixedRouter, rh.c.MetaDB, rh.c.Log) diff --git a/pkg/extensions/README.md b/pkg/extensions/README.md index a6cb9ba33..6f9df76ba 100644 --- a/pkg/extensions/README.md +++ b/pkg/extensions/README.md @@ -30,9 +30,9 @@ package extensions IsAdmin bool Username string Groups []string - } + } ``` - This data can then be accessed from the request context so that every extension can apply its own authorization logic, if needed . + This data can then be accessed from the request context so that every extension can apply its own authorization logic, if needed . - when a new extension comes out, the developer should also write some blackbox tests, where a binary that contains the new extension should be tested in a real usage scenario. See [test/blackbox](test/blackbox/sync.bats) folder for multiple extensions examples. @@ -40,6 +40,6 @@ package extensions - with every new extension, you should modify the EXTENSIONS variable in Makefile by adding the new extension. The EXTENSIONS variable represents all extensions and is used in Make targets that require them all (e.g make test). -- the available extensions that can be used at the moment are: sync, scrub, metrics, search . +- the available extensions that can be used at the moment are: sync, search, scrub, metrics, lint, ui, mgmt, userprefs, imagetrust . NOTE: When multiple extensions are used, they should be listed in the above presented order. diff --git a/pkg/extensions/extension_metrics.go b/pkg/extensions/extension_metrics.go index 54e71982f..1837d7ef7 100644 --- a/pkg/extensions/extension_metrics.go +++ b/pkg/extensions/extension_metrics.go @@ -26,13 +26,14 @@ func EnableMetricsExtension(config *config.Config, log log.Logger, rootDir strin } func SetupMetricsRoutes(config *config.Config, router *mux.Router, - authFunc mux.MiddlewareFunc, log log.Logger, metrics monitoring.MetricServer, + authnFunc, authzFunc mux.MiddlewareFunc, log log.Logger, metrics monitoring.MetricServer, ) { log.Info().Msg("setting up metrics routes") if config.IsMetricsEnabled() { extRouter := router.PathPrefix(config.Extensions.Metrics.Prometheus.Path).Subrouter() - extRouter.Use(authFunc) + extRouter.Use(authnFunc) + extRouter.Use(authzFunc) extRouter.Methods("GET").Handler(promhttp.Handler()) } } diff --git a/pkg/extensions/extension_metrics_disabled.go b/pkg/extensions/extension_metrics_disabled.go index 6d280b9ba..624fc0612 100644 --- a/pkg/extensions/extension_metrics_disabled.go +++ b/pkg/extensions/extension_metrics_disabled.go @@ -22,13 +22,14 @@ func EnableMetricsExtension(config *config.Config, log log.Logger, rootDir strin // SetupMetricsRoutes ... func SetupMetricsRoutes(conf *config.Config, router *mux.Router, - authFunc mux.MiddlewareFunc, log log.Logger, metrics monitoring.MetricServer, + authnFunc, authzFunc mux.MiddlewareFunc, log log.Logger, metrics monitoring.MetricServer, ) { getMetrics := func(w http.ResponseWriter, r *http.Request) { m := metrics.ReceiveMetrics() zcommon.WriteJSON(w, http.StatusOK, m) } - router.Use(authFunc) + router.Use(authnFunc) + router.Use(authzFunc) router.HandleFunc("/metrics", getMetrics).Methods("GET") } diff --git a/pkg/extensions/monitoring/monitoring_test.go b/pkg/extensions/monitoring/monitoring_test.go index 9b5089f01..026389b6b 100644 --- a/pkg/extensions/monitoring/monitoring_test.go +++ b/pkg/extensions/monitoring/monitoring_test.go @@ -4,7 +4,9 @@ package monitoring_test import ( + "math/rand" "net/http" + "os" "testing" "time" @@ -103,3 +105,323 @@ func TestExtensionMetrics(t *testing.T) { So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) }) } + +func TestMetricsAuthentication(t *testing.T) { + Convey("test metrics without authentication and metrics enabled", t, func() { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = t.TempDir() + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() + + // metrics endpoint not available + resp, err := resty.R().Get(baseURL + "/metrics") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) + }) + Convey("test metrics without authentication and with metrics enabled", t, func() { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + enabled := true + metricsConfig := &extconf.MetricsConfig{ + BaseConfig: extconf.BaseConfig{Enable: &enabled}, + Prometheus: &extconf.PrometheusConfig{Path: "/metrics"}, + } + conf.Extensions = &extconf.ExtensionConfig{ + Metrics: metricsConfig, + } + + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = t.TempDir() + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() + + // without auth set metrics endpoint is available + resp, err := resty.R().Get(baseURL + "/metrics") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + }) + Convey("test metrics with authentication and metrics enabled", t, func() { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + + username := generateRandomString() + password := generateRandomString() + metricsuser := generateRandomString() + metricspass := generateRandomString() + content := test.GetCredString(username, password) + "\n" + test.GetCredString(metricsuser, metricspass) + htpasswdPath := test.MakeHtpasswdFileFromString(content) + defer os.Remove(htpasswdPath) + + conf.HTTP.Auth = &config.AuthConfig{ + HTPasswd: config.AuthHTPasswd{ + Path: htpasswdPath, + }, + } + + enabled := true + metricsConfig := &extconf.MetricsConfig{ + BaseConfig: extconf.BaseConfig{Enable: &enabled}, + Prometheus: &extconf.PrometheusConfig{Path: "/metrics"}, + } + conf.Extensions = &extconf.ExtensionConfig{ + Metrics: metricsConfig, + } + + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = t.TempDir() + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() + + // without credentials + resp, err := resty.R().Get(baseURL + "/metrics") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + + // with wrong credentials + resp, err = resty.R().SetBasicAuth("atacker", "wrongpassword").Get(baseURL + "/metrics") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + + // authenticated users + resp, err = resty.R().SetBasicAuth(username, password).Get(baseURL + "/metrics") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + resp, err = resty.R().SetBasicAuth(metricsuser, metricspass).Get(baseURL + "/metrics") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + }) +} + +func TestMetricsAuthorization(t *testing.T) { + const AuthorizationAllRepos = "**" + + Convey("Make a new controller with auth & metrics enabled", t, func() { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + + username := generateRandomString() + password := generateRandomString() + metricsuser := generateRandomString() + metricspass := generateRandomString() + content := test.GetCredString(username, password) + "\n" + test.GetCredString(metricsuser, metricspass) + htpasswdPath := test.MakeHtpasswdFileFromString(content) + defer os.Remove(htpasswdPath) + + conf.HTTP.Auth = &config.AuthConfig{ + HTPasswd: config.AuthHTPasswd{ + Path: htpasswdPath, + }, + } + + enabled := true + metricsConfig := &extconf.MetricsConfig{ + BaseConfig: extconf.BaseConfig{Enable: &enabled}, + Prometheus: &extconf.PrometheusConfig{Path: "/metrics"}, + } + conf.Extensions = &extconf.ExtensionConfig{ + Metrics: metricsConfig, + } + + Convey("with basic auth: no metrics users in accessControl", func() { + conf.HTTP.AccessControl = &config.AccessControlConfig{ + Metrics: config.Metrics{ + Users: []string{}, + }, + } + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = t.TempDir() + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() + + // authenticated but not authorized user should not have access to/metrics + client := resty.New() + client.SetBasicAuth(username, password) + resp, err := client.R().Get(baseURL + "/metrics") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + + // authenticated but not authorized user should not have access to/metrics + client.SetBasicAuth(metricsuser, metricspass) + resp, err = client.R().Get(baseURL + "/metrics") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + }) + Convey("with basic auth: metrics users in accessControl", func() { + conf.HTTP.AccessControl = &config.AccessControlConfig{ + Metrics: config.Metrics{ + Users: []string{metricsuser}, + }, + } + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = t.TempDir() + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() + + // authenticated but not authorized user should not have access to/metrics + client := resty.New() + client.SetBasicAuth(username, password) + resp, err := client.R().Get(baseURL + "/metrics") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // authenticated & authorized user should have access to/metrics + client.SetBasicAuth(metricsuser, metricspass) + resp, err = client.R().Get(baseURL + "/metrics") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + }) + Convey("with basic auth: with anonymousPolicy in accessControl", func() { + conf.HTTP.AccessControl = &config.AccessControlConfig{ + Metrics: config.Metrics{ + Users: []string{metricsuser}, + }, + Repositories: config.Repositories{ + AuthorizationAllRepos: config.PolicyGroup{ + Policies: []config.Policy{ + { + Users: []string{}, + Actions: []string{}, + }, + }, + AnonymousPolicy: []string{"read"}, + DefaultPolicy: []string{}, + }, + }, + } + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = t.TempDir() + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() + + // unauthenticated clients should not have access to /metrics + resp, err := resty.R().Get(baseURL + "/metrics") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + + // unauthenticated clients should not have access to /metrics + resp, err = resty.R().SetBasicAuth("hacker", "trywithwrongpass").Get(baseURL + "/metrics") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + + // authenticated but not authorized user should not have access to/metrics + client := resty.New() + client.SetBasicAuth(username, password) + resp, err = client.R().Get(baseURL + "/metrics") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // authenticated & authorized user should have access to/metrics + client.SetBasicAuth(metricsuser, metricspass) + resp, err = client.R().Get(baseURL + "/metrics") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + }) + Convey("with basic auth: with adminPolicy in accessControl", func() { + conf.HTTP.AccessControl = &config.AccessControlConfig{ + Metrics: config.Metrics{ + Users: []string{metricsuser}, + }, + Repositories: config.Repositories{ + AuthorizationAllRepos: config.PolicyGroup{ + Policies: []config.Policy{ + { + Users: []string{}, + Actions: []string{}, + }, + }, + DefaultPolicy: []string{}, + }, + }, + AdminPolicy: config.Policy{ + Users: []string{"test"}, + Groups: []string{"admins"}, + Actions: []string{"read", "create", "update", "delete"}, + }, + } + ctlr := api.NewController(conf) + ctlr.Config.Storage.RootDirectory = t.TempDir() + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() + + // unauthenticated clients should not have access to /metrics + resp, err := resty.R().Get(baseURL + "/metrics") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + + // unauthenticated clients should not have access to /metrics + resp, err = resty.R().SetBasicAuth("hacker", "trywithwrongpass").Get(baseURL + "/metrics") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + + // authenticated admin user (but not authorized) should not have access to/metrics + client := resty.New() + client.SetBasicAuth(username, password) + resp, err = client.R().Get(baseURL + "/metrics") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // authenticated & authorized user should have access to/metrics + client.SetBasicAuth(metricsuser, metricspass) + resp, err = client.R().Get(baseURL + "/metrics") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + }) + }) +} + +func generateRandomString() string { + //nolint: gosec + seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) + charset := "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + randomBytes := make([]byte, 10) + for i := range randomBytes { + randomBytes[i] = charset[seededRand.Intn(len(charset))] + } + + return string(randomBytes) +} diff --git a/pkg/test/skip/skip_test.go b/pkg/test/skip/skip_test.go new file mode 100644 index 000000000..b266f11cd --- /dev/null +++ b/pkg/test/skip/skip_test.go @@ -0,0 +1,37 @@ +package skip_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + + tskip "zotregistry.io/zot/pkg/test/skip" +) + +// for code coverage. +func TestSkipS3(t *testing.T) { + envName := "S3MOCK_ENDPOINT" + envVal := os.Getenv(envName) + + if len(envVal) > 0 { + defer os.Setenv(envName, envVal) + err := os.Unsetenv(envName) + assert.Equal(t, err, nil, "Error should be nil") + } + + tskip.SkipS3(t) +} + +func TestSkipDynamo(t *testing.T) { + envName := "DYNAMODBMOCK_ENDPOINT" + envVal := os.Getenv(envName) + + if len(envVal) > 0 { + defer os.Setenv(envName, envVal) + err := os.Unsetenv(envName) + assert.Equal(t, err, nil, "Error should be nil") + } + + tskip.SkipDynamo(t) +} diff --git a/test/blackbox/helpers_metrics.bash b/test/blackbox/helpers_metrics.bash index cce55e5b8..4c225568b 100644 --- a/test/blackbox/helpers_metrics.bash +++ b/test/blackbox/helpers_metrics.bash @@ -1,3 +1,6 @@ +METRICS_USER=observability +METRICS_PASS=MySecreTPa55 + function metrics_route_check () { local servername="http://127.0.0.1:${1}/metrics" status_code=$(curl --write-out '%{http_code}' ${2} --silent --output /dev/null ${servername}) diff --git a/test/blackbox/metrics.bats b/test/blackbox/metrics.bats index 0d392ac2c..c490c541a 100644 --- a/test/blackbox/metrics.bats +++ b/test/blackbox/metrics.bats @@ -32,6 +32,7 @@ function setup_file() { zot_config_file=${BATS_FILE_TMPDIR}/zot_config.json zot_htpasswd_file=${BATS_FILE_TMPDIR}/zot_htpasswd htpasswd -Bbn ${AUTH_USER} ${AUTH_PASS} >> ${zot_htpasswd_file} + htpasswd -Bbn ${METRICS_USER} ${METRICS_PASS} >> ${zot_htpasswd_file} mkdir -p ${zot_root_dir} touch ${zot_log_file} @@ -48,6 +49,19 @@ function setup_file() { "htpasswd": { "path": "${zot_htpasswd_file}" } + }, + "accessControl": { + "metrics":{ + "users": ["${METRICS_USER}"] + }, + "repositories": { + "**": { + "anonymousPolicy": [ + "read" + ], + "defaultPolicy": ["read","create"] + } + } } }, "log": { @@ -80,14 +94,20 @@ function teardown_file() { } @test "unauthorized request to metrics" { +# anonymous policy: metrics endpoint should not be available +# 401 - http.StatusUnauthorized run metrics_route_check 8080 "" 401 [ "$status" -eq 0 ] +# user is not in htpasswd run metrics_route_check 8080 "-u unlucky:wrongpass" 401 [ "$status" -eq 0 ] +# proper user/pass tuple from htpasswd, but user not allowed to access metrics +# 403 - http.StatusForbidden + run metrics_route_check 8080 "-u ${AUTH_USER}:${AUTH_PASS}" 403 + [ "$status" -eq 0 ] } @test "authorized request: metrics enabled" { - run metrics_route_check 8080 "-u ${AUTH_USER}:${AUTH_PASS}" 200 + run metrics_route_check 8080 "-u ${METRICS_USER}:${METRICS_PASS}" 200 [ "$status" -eq 0 ] } - diff --git a/test/blackbox/metrics_minimal.bats b/test/blackbox/metrics_minimal.bats index c693f64ec..352498840 100644 --- a/test/blackbox/metrics_minimal.bats +++ b/test/blackbox/metrics_minimal.bats @@ -32,6 +32,7 @@ function setup_file() { zot_config_file=${BATS_FILE_TMPDIR}/zot_config.json zot_htpasswd_file=${BATS_FILE_TMPDIR}/zot_htpasswd htpasswd -Bbn ${AUTH_USER} ${AUTH_PASS} >> ${zot_htpasswd_file} + htpasswd -Bbn ${METRICS_USER} ${METRICS_PASS} >> ${zot_htpasswd_file} mkdir -p ${zot_root_dir} touch ${zot_log_file} @@ -48,6 +49,20 @@ function setup_file() { "htpasswd": { "path": "${zot_htpasswd_file}" } + }, + "accessControl": { + "metrics":{ + "users": ["${METRICS_USER}"] + }, + "repositories": { + "**": { + "anonymousPolicy": [ + "read", + "create" + ], + "defaultPolicy": ["read"] + } + } } }, "log": { @@ -72,13 +87,20 @@ function teardown_file() { } @test "unauthorized request to metrics" { +# anonymous policy: metrics endpoint should not be available +# 401 - http.StatusUnauthorized run metrics_route_check 8080 "" 401 [ "$status" -eq 0 ] +# user is not in htpasswd run metrics_route_check 8080 "-u test:wrongpass" 401 [ "$status" -eq 0 ] +# proper user/pass tuple from htpasswd, but user not allowed to access metrics +# 403 - http.StatusForbidden + run metrics_route_check 8080 "-u ${AUTH_USER}:${AUTH_PASS}" 403 + [ "$status" -eq 0 ] } @test "authorized request: metrics enabled" { - run metrics_route_check 8080 "-u ${AUTH_USER}:${AUTH_PASS}" 200 + run metrics_route_check 8080 "-u ${METRICS_USER}:${METRICS_PASS}" 200 [ "$status" -eq 0 ] -} \ No newline at end of file +}