Files
zot/pkg/common/http_server.go
T
Andrei Aaron dfb5d1df54 fix: make config read/write thread safe (#3432)
* fix: make config read/write thread safe and fix some other similar issues

1. The config config has a lock, and safe methods to update and read the attributes
2. The config has methods to retrieve copies of specific attributes, such as the extyensions config, the auth config, and the authz config.
These are needed, as the config object may mutate in the middle of an auth/authz requests, and we avoid partial configuration being applied for that request.
3. Fix an issue with the monitoring server not stopping when the controller is shut down.
4. Fix an issue with the HTPasswdWatcher not stopping when the background tasks are supposed to finish.
5. Fix some tests using hardcoded ports.

Moved some of the methods which were on the main config to the auth, access control and extension configs

Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
2025-10-18 11:20:58 +03:00

158 lines
4.3 KiB
Go

package common
import (
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
jsoniter "github.com/json-iterator/go"
"zotregistry.dev/zot/v2/pkg/api/config"
"zotregistry.dev/zot/v2/pkg/api/constants"
apiErr "zotregistry.dev/zot/v2/pkg/api/errors"
reqCtx "zotregistry.dev/zot/v2/pkg/requestcontext"
)
func AllowedMethods(methods ...string) []string {
return append(methods, http.MethodOptions)
}
func AddExtensionSecurityHeaders() mux.MiddlewareFunc { //nolint:varnamelen
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
resp.Header().Set("X-Content-Type-Options", "nosniff")
next.ServeHTTP(resp, req)
})
}
}
func ACHeadersMiddleware(config *config.Config, allowedMethods ...string) mux.MiddlewareFunc {
allowedMethodsValue := strings.Join(allowedMethods, ",")
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
resp.Header().Set("Access-Control-Allow-Methods", allowedMethodsValue)
resp.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type,"+constants.SessionClientHeaderName)
// Get auth config safely
authConfig := config.CopyAuthConfig()
if authConfig.IsBasicAuthnEnabled() {
resp.Header().Set("Access-Control-Allow-Credentials", "true")
}
if req.Method == http.MethodOptions {
return
}
next.ServeHTTP(resp, req)
})
}
}
func CORSHeadersMiddleware(allowOrigin string) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
AddCORSHeaders(allowOrigin, response)
next.ServeHTTP(response, request)
})
}
}
func AddCORSHeaders(allowOrigin string, response http.ResponseWriter) {
if allowOrigin == "" {
response.Header().Set("Access-Control-Allow-Origin", "*")
} else {
response.Header().Set("Access-Control-Allow-Origin", allowOrigin)
}
}
// AuthzOnlyAdminsMiddleware permits only admin user access if auth is enabled.
func AuthzOnlyAdminsMiddleware(conf *config.Config) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
// Get auth config safely
authConfig := conf.CopyAuthConfig()
if !authConfig.IsBasicAuthnEnabled() {
next.ServeHTTP(response, request)
return
}
realm := conf.GetRealm()
failDelay := authConfig.GetFailDelay()
// get userAccessControl built in previous authn/authz middlewares
userAc, err := reqCtx.UserAcFromContext(request.Context())
if err != nil { // should not happen as this has been previously checked for errors
AuthzFail(response, request, userAc.GetUsername(), realm, failDelay)
return
}
// reject non-admin access if authentication is enabled
if userAc != nil && !userAc.IsAdmin() {
AuthzFail(response, request, userAc.GetUsername(), realm, failDelay)
return
}
next.ServeHTTP(response, request)
})
}
}
func AuthzFail(w http.ResponseWriter, r *http.Request, identity, realm string, delay int) {
time.Sleep(time.Duration(delay) * time.Second)
// don't send auth headers if request is coming from UI
if r.Header.Get(constants.SessionClientHeaderName) != constants.SessionClientHeaderValue {
if realm == "" {
realm = "Authorization Required"
}
realm = "Basic realm=" + strconv.Quote(realm)
w.Header().Set("WWW-Authenticate", realm)
}
w.Header().Set("Content-Type", "application/json")
if identity == "" {
WriteJSON(w, http.StatusUnauthorized, apiErr.NewErrorList(apiErr.NewError(apiErr.UNAUTHORIZED)))
} else {
WriteJSON(w, http.StatusForbidden, apiErr.NewErrorList(apiErr.NewError(apiErr.DENIED)))
}
}
func WriteJSON(response http.ResponseWriter, status int, data interface{}) {
json := jsoniter.ConfigCompatibleWithStandardLibrary
body, err := json.Marshal(data)
if err != nil {
panic(err)
}
WriteData(response, status, constants.DefaultMediaType, body)
}
func WriteData(w http.ResponseWriter, status int, mediaType string, data []byte) {
w.Header().Set("Content-Type", mediaType)
w.WriteHeader(status)
_, _ = w.Write(data)
}
func QueryHasParams(values url.Values, params []string) bool {
for _, param := range params {
if !values.Has(param) {
return false
}
}
return true
}