0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2025-02-17 23:45:36 -05:00

fix: metrics should be protected behind authZ (#1895)

Signed-off-by: Alexei Dodon <adodon@cisco.com>
This commit is contained in:
Alexei Dodon 2023-10-20 10:33:26 +03:00 committed by GitHub
parent a44ca578a1
commit a345ba0823
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 508 additions and 31 deletions

View file

@ -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"
}
}
}
}

View file

@ -57,7 +57,7 @@ func AuthHandler(ctlr *Controller) mux.MiddlewareFunc {
return bearerAuthHandler(ctlr) return bearerAuthHandler(ctlr)
} }
return authnMiddleware.TryAuthnHandlers(ctlr) return authnMiddleware.tryAuthnHandlers(ctlr)
} }
func (amw *AuthnMiddleware) sessionAuthn(ctlr *Controller, userAc *reqCtx.UserAccessControl, 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 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 // no password based authN, if neither LDAP nor HTTP BASIC is enabled
if !ctlr.Config.IsBasicAuthnEnabled() { if !ctlr.Config.IsBasicAuthnEnabled() {
return noPasswdAuth(ctlr) return noPasswdAuth(ctlr)

View file

@ -191,14 +191,10 @@ func (ac *AccessController) getAuthnMiddlewareContext(authnType string, request
func (ac *AccessController) isPermitted(userGroups []string, username, action string, func (ac *AccessController) isPermitted(userGroups []string, username, action string,
policyGroup config.PolicyGroup, policyGroup config.PolicyGroup,
) bool { ) bool {
var result bool
// check repo/system based policies // check repo/system based policies
for _, p := range policyGroup.Policies { for _, p := range policyGroup.Policies {
if common.Contains(p.Users, username) && common.Contains(p.Actions, action) { if common.Contains(p.Users, username) && common.Contains(p.Actions, action) {
result = true return true
return result
} }
} }
@ -207,9 +203,7 @@ func (ac *AccessController) isPermitted(userGroups []string, username, action st
if common.Contains(p.Actions, action) { if common.Contains(p.Actions, action) {
for _, group := range p.Groups { for _, group := range p.Groups {
if common.Contains(userGroups, group) { if common.Contains(userGroups, group) {
result = true return true
return result
} }
} }
} }
@ -217,20 +211,16 @@ func (ac *AccessController) isPermitted(userGroups []string, username, action st
} }
// check defaultPolicy // check defaultPolicy
if !result { if common.Contains(policyGroup.DefaultPolicy, action) && username != "" {
if common.Contains(policyGroup.DefaultPolicy, action) && username != "" { return true
result = true
}
} }
// check anonymousPolicy // check anonymousPolicy
if !result { if common.Contains(policyGroup.AnonymousPolicy, action) && username == "" {
if common.Contains(policyGroup.AnonymousPolicy, action) && username == "" { return true
result = true
}
} }
return result return false
} }
func BaseAuthzHandler(ctlr *Controller) mux.MiddlewareFunc { 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
})
}
}

View file

@ -131,6 +131,7 @@ type AccessControlConfig struct {
Repositories Repositories `json:"repositories" mapstructure:"repositories"` Repositories Repositories `json:"repositories" mapstructure:"repositories"`
AdminPolicy Policy AdminPolicy Policy
Groups Groups Groups Groups
Metrics Metrics
} }
func (config *AccessControlConfig) AnonymousPolicyExists() bool { func (config *AccessControlConfig) AnonymousPolicyExists() bool {
@ -168,6 +169,10 @@ type Policy struct {
Groups []string Groups []string
} }
type Metrics struct {
Users []string
}
type Config struct { type Config struct {
DistSpecVersion string `json:"distSpecVersion" mapstructure:"distSpecVersion"` DistSpecVersion string `json:"distSpecVersion" mapstructure:"distSpecVersion"`
GoVersion string GoVersion string

View file

@ -183,7 +183,7 @@ func (rh *RouteHandler) SetupRoutes() {
pprof.SetupPprofRoutes(rh.c.Config, prefixedRouter, authHandler, rh.c.Log) pprof.SetupPprofRoutes(rh.c.Config, prefixedRouter, authHandler, rh.c.Log)
// Preconditions for enabling the actual extension routes are part of extensions themselves // 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, ext.SetupSearchRoutes(rh.c.Config, prefixedRouter, rh.c.StoreController, rh.c.MetaDB, rh.c.CveScanner,
rh.c.Log) rh.c.Log)
ext.SetupImageTrustRoutes(rh.c.Config, prefixedRouter, rh.c.MetaDB, rh.c.Log) ext.SetupImageTrustRoutes(rh.c.Config, prefixedRouter, rh.c.MetaDB, rh.c.Log)

View file

@ -30,9 +30,9 @@ package extensions
IsAdmin bool IsAdmin bool
Username string Username string
Groups []string Groups []string
} }
``` ```
This data can then be accessed from the request context so that <b>every extension can apply its own authorization logic, if needed </b>. This data can then be accessed from the request context so that <b>every extension can apply its own authorization logic, if needed </b>.
- 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. - 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). - 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: <b>sync, scrub, metrics, search </b>. - the available extensions that can be used at the moment are: <b>sync, search, scrub, metrics, lint, ui, mgmt, userprefs, imagetrust </b>.
NOTE: When multiple extensions are used, they should be listed in the above presented order. NOTE: When multiple extensions are used, they should be listed in the above presented order.

View file

@ -26,13 +26,14 @@ func EnableMetricsExtension(config *config.Config, log log.Logger, rootDir strin
} }
func SetupMetricsRoutes(config *config.Config, router *mux.Router, 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") log.Info().Msg("setting up metrics routes")
if config.IsMetricsEnabled() { if config.IsMetricsEnabled() {
extRouter := router.PathPrefix(config.Extensions.Metrics.Prometheus.Path).Subrouter() extRouter := router.PathPrefix(config.Extensions.Metrics.Prometheus.Path).Subrouter()
extRouter.Use(authFunc) extRouter.Use(authnFunc)
extRouter.Use(authzFunc)
extRouter.Methods("GET").Handler(promhttp.Handler()) extRouter.Methods("GET").Handler(promhttp.Handler())
} }
} }

View file

@ -22,13 +22,14 @@ func EnableMetricsExtension(config *config.Config, log log.Logger, rootDir strin
// SetupMetricsRoutes ... // SetupMetricsRoutes ...
func SetupMetricsRoutes(conf *config.Config, router *mux.Router, 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) { getMetrics := func(w http.ResponseWriter, r *http.Request) {
m := metrics.ReceiveMetrics() m := metrics.ReceiveMetrics()
zcommon.WriteJSON(w, http.StatusOK, m) zcommon.WriteJSON(w, http.StatusOK, m)
} }
router.Use(authFunc) router.Use(authnFunc)
router.Use(authzFunc)
router.HandleFunc("/metrics", getMetrics).Methods("GET") router.HandleFunc("/metrics", getMetrics).Methods("GET")
} }

View file

@ -4,7 +4,9 @@
package monitoring_test package monitoring_test
import ( import (
"math/rand"
"net/http" "net/http"
"os"
"testing" "testing"
"time" "time"
@ -103,3 +105,323 @@ func TestExtensionMetrics(t *testing.T) {
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) 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)
}

View file

@ -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)
}

View file

@ -1,3 +1,6 @@
METRICS_USER=observability
METRICS_PASS=MySecreTPa55
function metrics_route_check () { function metrics_route_check () {
local servername="http://127.0.0.1:${1}/metrics" local servername="http://127.0.0.1:${1}/metrics"
status_code=$(curl --write-out '%{http_code}' ${2} --silent --output /dev/null ${servername}) status_code=$(curl --write-out '%{http_code}' ${2} --silent --output /dev/null ${servername})

View file

@ -32,6 +32,7 @@ function setup_file() {
zot_config_file=${BATS_FILE_TMPDIR}/zot_config.json zot_config_file=${BATS_FILE_TMPDIR}/zot_config.json
zot_htpasswd_file=${BATS_FILE_TMPDIR}/zot_htpasswd zot_htpasswd_file=${BATS_FILE_TMPDIR}/zot_htpasswd
htpasswd -Bbn ${AUTH_USER} ${AUTH_PASS} >> ${zot_htpasswd_file} htpasswd -Bbn ${AUTH_USER} ${AUTH_PASS} >> ${zot_htpasswd_file}
htpasswd -Bbn ${METRICS_USER} ${METRICS_PASS} >> ${zot_htpasswd_file}
mkdir -p ${zot_root_dir} mkdir -p ${zot_root_dir}
touch ${zot_log_file} touch ${zot_log_file}
@ -48,6 +49,19 @@ function setup_file() {
"htpasswd": { "htpasswd": {
"path": "${zot_htpasswd_file}" "path": "${zot_htpasswd_file}"
} }
},
"accessControl": {
"metrics":{
"users": ["${METRICS_USER}"]
},
"repositories": {
"**": {
"anonymousPolicy": [
"read"
],
"defaultPolicy": ["read","create"]
}
}
} }
}, },
"log": { "log": {
@ -80,14 +94,20 @@ function teardown_file() {
} }
@test "unauthorized request to metrics" { @test "unauthorized request to metrics" {
# anonymous policy: metrics endpoint should not be available
# 401 - http.StatusUnauthorized
run metrics_route_check 8080 "" 401 run metrics_route_check 8080 "" 401
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
# user is not in htpasswd
run metrics_route_check 8080 "-u unlucky:wrongpass" 401 run metrics_route_check 8080 "-u unlucky:wrongpass" 401
[ "$status" -eq 0 ] [ "$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" { @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 ] [ "$status" -eq 0 ]
} }

View file

@ -32,6 +32,7 @@ function setup_file() {
zot_config_file=${BATS_FILE_TMPDIR}/zot_config.json zot_config_file=${BATS_FILE_TMPDIR}/zot_config.json
zot_htpasswd_file=${BATS_FILE_TMPDIR}/zot_htpasswd zot_htpasswd_file=${BATS_FILE_TMPDIR}/zot_htpasswd
htpasswd -Bbn ${AUTH_USER} ${AUTH_PASS} >> ${zot_htpasswd_file} htpasswd -Bbn ${AUTH_USER} ${AUTH_PASS} >> ${zot_htpasswd_file}
htpasswd -Bbn ${METRICS_USER} ${METRICS_PASS} >> ${zot_htpasswd_file}
mkdir -p ${zot_root_dir} mkdir -p ${zot_root_dir}
touch ${zot_log_file} touch ${zot_log_file}
@ -48,6 +49,20 @@ function setup_file() {
"htpasswd": { "htpasswd": {
"path": "${zot_htpasswd_file}" "path": "${zot_htpasswd_file}"
} }
},
"accessControl": {
"metrics":{
"users": ["${METRICS_USER}"]
},
"repositories": {
"**": {
"anonymousPolicy": [
"read",
"create"
],
"defaultPolicy": ["read"]
}
}
} }
}, },
"log": { "log": {
@ -72,13 +87,20 @@ function teardown_file() {
} }
@test "unauthorized request to metrics" { @test "unauthorized request to metrics" {
# anonymous policy: metrics endpoint should not be available
# 401 - http.StatusUnauthorized
run metrics_route_check 8080 "" 401 run metrics_route_check 8080 "" 401
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
# user is not in htpasswd
run metrics_route_check 8080 "-u test:wrongpass" 401 run metrics_route_check 8080 "-u test:wrongpass" 401
[ "$status" -eq 0 ] [ "$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" { @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 ] [ "$status" -eq 0 ]
} }