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:
parent
a44ca578a1
commit
a345ba0823
13 changed files with 508 additions and 31 deletions
39
examples/config-metrics-authz.json
Normal file
39
examples/config-metrics-authz.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -30,9 +30,9 @@ package extensions
|
|||
IsAdmin bool
|
||||
Username 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.
|
||||
|
||||
|
@ -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: <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.
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
37
pkg/test/skip/skip_test.go
Normal file
37
pkg/test/skip/skip_test.go
Normal 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)
|
||||
}
|
|
@ -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})
|
||||
|
|
|
@ -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 ]
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue