feat(metrics): anonymous access when enabled in accessControl config (#4110)

* feat: add anonymouspolicy support in metrics

Signed-off-by: uaggarwa <uaggarwa@akamai.com>

* test: add unit tests

Signed-off-by: uaggarwa <uaggarwa@akamai.com>

---------

Signed-off-by: uaggarwa <uaggarwa@akamai.com>
This commit is contained in:
uaggarwa
2026-06-10 03:19:28 -04:00
committed by GitHub
parent 273b15364b
commit 66e9cfb01f
7 changed files with 334 additions and 5 deletions
+30 -3
View File
@@ -40,6 +40,7 @@ import (
"zotregistry.dev/zot/v2/pkg/api/constants"
apiErr "zotregistry.dev/zot/v2/pkg/api/errors"
zcommon "zotregistry.dev/zot/v2/pkg/common"
extconf "zotregistry.dev/zot/v2/pkg/extensions/config"
"zotregistry.dev/zot/v2/pkg/log"
reqCtx "zotregistry.dev/zot/v2/pkg/requestcontext"
)
@@ -432,6 +433,10 @@ func (amw *AuthnMiddleware) tryAuthnHandlers(ctlr *Controller) mux.MiddlewareFun
accessControlConfig := ctlr.Config.CopyAccessControlConfig()
allowAnonymous := accessControlConfig != nil && accessControlConfig.AnonymousPolicyExists()
// Allow anonymous access to the metrics endpoint only if configured.
extensionsConfig := ctlr.Config.CopyExtensionsConfig()
isMetricsRequestedWithAnonymousAccess := isAnonymousMetricsRequest(request, accessControlConfig, extensionsConfig)
// build user access control info
userAc := reqCtx.NewUserAccessControl()
// if it will not be populated by authn handlers, this represents an anonymous user
@@ -461,7 +466,7 @@ func (amw *AuthnMiddleware) tryAuthnHandlers(ctlr *Controller) mux.MiddlewareFun
// If session authentication fails, but anonymous or management access is allowed,
// treat the request as authenticated. This fallback is necessary because the session
// header may be present for anonymous or management requests.
authenticated = authenticated || allowAnonymous || isMgmtRequested
authenticated = authenticated || allowAnonymous || isMgmtRequested || isMetricsRequestedWithAnonymousAccess
// Try mTLS authentication if client certificates are present
case ctlr.Config.IsMTLSAuthEnabled() && request.TLS != nil && len(request.TLS.PeerCertificates) > 0:
@@ -471,8 +476,8 @@ func (amw *AuthnMiddleware) tryAuthnHandlers(ctlr *Controller) mux.MiddlewareFun
case !authConfig.IsBasicAuthnEnabled() && !ctlr.Config.IsMTLSAuthEnabled():
authenticated = true
// If no credentials provided - check for anonymous / mgmt requests
case allowAnonymous || isMgmtRequested:
// If no credentials provided - check for anonymous / mgmt / metrics requests
case allowAnonymous || isMgmtRequested || isMetricsRequestedWithAnonymousAccess:
// Docker workaround: force 401 on /v2/ when anonymous policies coexist with
// authenticated-only policies. Otherwise Docker treats 200 on /v2/ as "no auth"
// and will not send stored credentials for protected repositories.
@@ -582,6 +587,16 @@ func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
return
}
// Allow anonymous access to the metrics endpoint only if configured.
accessControlConfig := ctlr.Config.CopyAccessControlConfig()
extensionsConfig := ctlr.Config.CopyExtensionsConfig()
if isAnonymousMetricsRequest(request, accessControlConfig, extensionsConfig) {
next.ServeHTTP(response, request)
return
}
var requestedAccess *ResourceAction
if request.RequestURI != "/v2/" {
@@ -977,6 +992,18 @@ func authFail(w http.ResponseWriter, r *http.Request, realm string, delay int) {
zcommon.WriteJSON(w, http.StatusUnauthorized, apiErr.NewError(apiErr.UNAUTHORIZED))
}
func isAnonymousMetricsRequest(request *http.Request, accessControlConfig *config.AccessControlConfig,
extensionsConfig *extconf.ExtensionConfig,
) bool {
prometheusConfig := extensionsConfig.GetMetricsPrometheusConfig()
return isAuthorizationHeaderEmpty(request) &&
slices.Contains(accessControlConfig.GetMetrics().AnonymousPolicy, constants.ReadPermission) &&
extensionsConfig.IsMetricsEnabled() &&
prometheusConfig != nil &&
strings.HasPrefix(request.URL.Path, prometheusConfig.Path)
}
func isAuthorizationHeaderEmpty(request *http.Request) bool {
header := request.Header.Get("Authorization")
+6
View File
@@ -742,6 +742,12 @@ func MetricsAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
}
metricsConfig := accessControlConfig.GetMetrics()
if slices.Contains(metricsConfig.AnonymousPolicy, constants.ReadPermission) {
next.ServeHTTP(response, request)
return
}
if len(metricsConfig.Users) == 0 {
log := ctlr.Log
log.Warn().Msg("auth is enabled but no metrics users in accessControl: /metrics is unaccesible")
+18 -1
View File
@@ -650,6 +650,22 @@ func (config *AccessControlConfig) GetMetrics() Metrics {
return config.Metrics
}
// ContainsOnlyMetricsAnonymousPolicy identifies an access control configuration
// that contains only an anonymous policy for metrics.
func (config *AccessControlConfig) ContainsOnlyMetricsAnonymousPolicy() bool {
if config == nil {
return false
}
admin := config.GetAdminPolicy()
if len(admin.Actions)+len(admin.Users)+len(admin.Groups) > 0 {
return false
}
return len(config.GetRepositories()) == 0 &&
len(config.GetGroups()) == 0 &&
slices.Contains(config.GetMetrics().AnonymousPolicy, "read")
}
// GetGroups safely gets a copy of the groups configuration.
func (config *AccessControlConfig) GetGroups() Groups {
if config == nil {
@@ -739,7 +755,8 @@ type Condition struct {
}
type Metrics struct {
Users []string
Users []string
AnonymousPolicy []string
}
type Config struct {