mirror of
https://github.com/project-zot/zot.git
synced 2024-12-30 22:34:13 -05:00
fix(authn): session authn is skipped when anonymous policy is configured (#1647)
closes: #1642 Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
This commit is contained in:
parent
abba6aa3cf
commit
635d71853e
5 changed files with 1063 additions and 819 deletions
2
go.mod
2
go.mod
|
@ -116,6 +116,7 @@ require (
|
|||
github.com/emicklei/go-restful/v3 v3.10.1 // indirect
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/go-errors/errors v1.4.2 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.0.5 // indirect
|
||||
|
@ -304,7 +305,6 @@ require (
|
|||
github.com/emicklei/proto v1.10.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fatih/color v1.14.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
|
|
215
pkg/api/authn.go
215
pkg/api/authn.go
|
@ -62,17 +62,10 @@ func AuthHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
return authnMiddleware.TryAuthnHandlers(ctlr)
|
||||
}
|
||||
|
||||
func (amw *AuthnMiddleware) sessionAuthn(ctlr *Controller, next http.Handler, response http.ResponseWriter,
|
||||
request *http.Request, delay int,
|
||||
) {
|
||||
clientHeader := request.Header.Get(constants.SessionClientHeaderName)
|
||||
if clientHeader != constants.SessionClientHeaderValue {
|
||||
authFail(response, request, ctlr.Config.HTTP.Realm, delay)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
identity, ok := common.GetAuthUserFromRequestSession(ctlr.CookieStore, request, ctlr.Log)
|
||||
func (amw *AuthnMiddleware) sessionAuthn(ctlr *Controller, response http.ResponseWriter,
|
||||
request *http.Request,
|
||||
) (bool, error) {
|
||||
identity, ok := GetAuthUserFromRequestSession(ctlr.CookieStore, request, ctlr.Log)
|
||||
if !ok {
|
||||
// let the client know that this session is invalid/expired
|
||||
cookie := &http.Cookie{
|
||||
|
@ -86,67 +79,34 @@ func (amw *AuthnMiddleware) sessionAuthn(ctlr *Controller, next http.Handler, re
|
|||
|
||||
http.SetCookie(response, cookie)
|
||||
|
||||
authFail(response, request, ctlr.Config.HTTP.Realm, delay)
|
||||
|
||||
return
|
||||
return false, nil
|
||||
}
|
||||
|
||||
ctx := getReqContextWithAuthorization(identity, []string{}, request)
|
||||
|
||||
groups, err := ctlr.MetaDB.GetUserGroups(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, zerr.ErrUserDataNotFound) {
|
||||
ctlr.Log.Err(err).Str("identity", identity).Msg("can not find user profile in DB")
|
||||
|
||||
authFail(response, request, ctlr.Config.HTTP.Realm, delay)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctlr.Log.Err(err).Str("identity", identity).Msg("can not get user profile in DB")
|
||||
|
||||
response.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
return false, err
|
||||
}
|
||||
|
||||
ctx = getReqContextWithAuthorization(identity, groups, request)
|
||||
*request = *request.WithContext(ctx)
|
||||
|
||||
next.ServeHTTP(response, request.WithContext(ctx))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (amw *AuthnMiddleware) basicAuthn(ctlr *Controller, response http.ResponseWriter,
|
||||
request *http.Request,
|
||||
) (bool, http.ResponseWriter, *http.Request, error) {
|
||||
) (bool, error) {
|
||||
cookieStore := ctlr.CookieStore
|
||||
|
||||
// we want to bypass auth for mgmt route
|
||||
isMgmtRequested := request.RequestURI == constants.FullMgmtPrefix
|
||||
|
||||
if request.Header.Get("Authorization") == "" {
|
||||
if ctlr.Config.HTTP.AccessControl.AnonymousPolicyExists() || isMgmtRequested {
|
||||
ctx := getReqContextWithAuthorization("", []string{}, request)
|
||||
// Process request
|
||||
|
||||
return true, response, request.WithContext(ctx), nil
|
||||
}
|
||||
}
|
||||
|
||||
identity, passphrase, err := getUsernamePasswordBasicAuth(request)
|
||||
if err != nil {
|
||||
ctlr.Log.Error().Err(err).Msg("failed to parse authorization header")
|
||||
|
||||
return false, nil, nil, nil
|
||||
}
|
||||
|
||||
// some client tools might send Authorization: Basic Og== (decoded into ":")
|
||||
// empty username and password
|
||||
if identity == "" && passphrase == "" {
|
||||
if ctlr.Config.HTTP.AccessControl.AnonymousPolicyExists() || isMgmtRequested {
|
||||
ctx := getReqContextWithAuthorization("", []string{}, request)
|
||||
|
||||
return true, response, request.WithContext(ctx), nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
passphraseHash, ok := amw.credMap[identity]
|
||||
|
@ -162,21 +122,22 @@ func (amw *AuthnMiddleware) basicAuthn(ctlr *Controller, response http.ResponseW
|
|||
}
|
||||
|
||||
ctx := getReqContextWithAuthorization(identity, groups, request)
|
||||
*request = *request.WithContext(ctx)
|
||||
|
||||
// saved logged session
|
||||
if err := saveUserLoggedSession(cookieStore, response, request, identity, ctlr.Log); err != nil {
|
||||
return false, response, request, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := ctlr.MetaDB.SetUserGroups(ctx, groups); err != nil {
|
||||
ctlr.Log.Error().Err(err).Str("identity", identity).Msg("couldn't update user profile")
|
||||
|
||||
return false, response, request, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
ctlr.Log.Info().Str("identity", identity).Msgf("user profile successfully set")
|
||||
|
||||
return true, response, request.WithContext(ctx), nil
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -195,29 +156,30 @@ func (amw *AuthnMiddleware) basicAuthn(ctlr *Controller, response http.ResponseW
|
|||
groups = append(groups, ldapgroups...)
|
||||
|
||||
ctx := getReqContextWithAuthorization(identity, groups, request)
|
||||
*request = *request.WithContext(ctx)
|
||||
|
||||
if err := saveUserLoggedSession(cookieStore, response, request, identity, ctlr.Log); err != nil {
|
||||
return false, response, request, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := ctlr.MetaDB.SetUserGroups(ctx, groups); err != nil {
|
||||
ctlr.Log.Error().Err(err).Str("identity", identity).Msg("couldn't update user profile")
|
||||
|
||||
return false, response, request, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, response, request.WithContext(ctx), nil
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
// last try API keys
|
||||
if isAPIKeyEnabled(ctlr.Config) {
|
||||
if ctlr.Config.IsAPIKeyEnabled() {
|
||||
apiKey := passphrase
|
||||
|
||||
if !strings.HasPrefix(apiKey, constants.APIKeysPrefix) {
|
||||
ctlr.Log.Error().Msg("api token has invalid format")
|
||||
|
||||
return false, nil, nil, nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
trimmedAPIKey := strings.TrimPrefix(apiKey, constants.APIKeysPrefix)
|
||||
|
@ -229,12 +191,12 @@ func (amw *AuthnMiddleware) basicAuthn(ctlr *Controller, response http.ResponseW
|
|||
if errors.Is(err, zerr.ErrUserAPIKeyNotFound) {
|
||||
ctlr.Log.Info().Err(err).Msgf("can not find any user info for hashed key %s in DB", hashedKey)
|
||||
|
||||
return false, nil, nil, nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
ctlr.Log.Error().Err(err).Msgf("can not get user info for hashed key %s in DB", hashedKey)
|
||||
|
||||
return false, nil, nil, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
if storedIdentity == identity {
|
||||
|
@ -244,30 +206,29 @@ func (amw *AuthnMiddleware) basicAuthn(ctlr *Controller, response http.ResponseW
|
|||
if err != nil {
|
||||
ctlr.Log.Err(err).Str("identity", identity).Msg("can not update user profile in DB")
|
||||
|
||||
return false, nil, nil, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
groups, err := ctlr.MetaDB.GetUserGroups(ctx)
|
||||
if err != nil {
|
||||
ctlr.Log.Err(err).Str("identity", identity).Msg("can not get user's groups in DB")
|
||||
|
||||
return false, nil, nil, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
ctx = getReqContextWithAuthorization(identity, groups, request)
|
||||
*request = *request.WithContext(ctx)
|
||||
|
||||
return true, response, request.WithContext(ctx), nil
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil, nil, nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
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.HTTP.Auth == nil ||
|
||||
(ctlr.Config.HTTP.Auth.HTPasswd.Path == "" && ctlr.Config.HTTP.Auth.LDAP == nil &&
|
||||
ctlr.Config.HTTP.Auth.OpenID == nil) {
|
||||
if !ctlr.Config.IsBasicAuthnEnabled() {
|
||||
return noPasswdAuth(ctlr.Config)
|
||||
}
|
||||
|
||||
|
@ -308,8 +269,7 @@ func (amw *AuthnMiddleware) TryAuthnHandlers(ctlr *Controller) mux.MiddlewareFun
|
|||
}
|
||||
|
||||
// ldap and htpasswd based authN
|
||||
if ctlr.Config.HTTP.Auth != nil {
|
||||
if ctlr.Config.HTTP.Auth.LDAP != nil {
|
||||
if ctlr.Config.IsLdapAuthEnabled() {
|
||||
ldapConfig := ctlr.Config.HTTP.Auth.LDAP
|
||||
amw.ldapClient = &LDAPClient{
|
||||
Host: ldapConfig.Address,
|
||||
|
@ -351,7 +311,7 @@ func (amw *AuthnMiddleware) TryAuthnHandlers(ctlr *Controller) mux.MiddlewareFun
|
|||
}
|
||||
}
|
||||
|
||||
if ctlr.Config.HTTP.Auth.HTPasswd.Path != "" {
|
||||
if ctlr.Config.IsHtpasswdAuthEnabled() {
|
||||
credsFile, err := os.Open(ctlr.Config.HTTP.Auth.HTPasswd.Path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -368,10 +328,9 @@ func (amw *AuthnMiddleware) TryAuthnHandlers(ctlr *Controller) mux.MiddlewareFun
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// openid based authN
|
||||
if ctlr.Config.HTTP.Auth.OpenID != nil {
|
||||
if ctlr.Config.IsOpenIDAuthEnabled() {
|
||||
ctlr.RelyingParties = make(map[string]rp.RelyingParty)
|
||||
|
||||
for provider := range ctlr.Config.HTTP.Auth.OpenID.Providers {
|
||||
|
@ -394,22 +353,57 @@ func (amw *AuthnMiddleware) TryAuthnHandlers(ctlr *Controller) mux.MiddlewareFun
|
|||
return
|
||||
}
|
||||
|
||||
// try basic auth if authorization header is given
|
||||
if !isAuthorizationHeaderEmpty(request) { //nolint: gocritic
|
||||
//nolint: contextcheck
|
||||
authenticated, cloneResp, cloneReq, err := amw.basicAuthn(ctlr, response, request)
|
||||
authenticated, err := amw.basicAuthn(ctlr, response, request)
|
||||
if err != nil {
|
||||
response.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if authenticated && cloneResp != nil && cloneReq != nil {
|
||||
next.ServeHTTP(cloneResp, cloneReq)
|
||||
if authenticated {
|
||||
next.ServeHTTP(response, request)
|
||||
|
||||
return
|
||||
}
|
||||
} else if hasSessionHeader(request) {
|
||||
// try session auth
|
||||
//nolint: contextcheck
|
||||
authenticated, err := amw.sessionAuthn(ctlr, response, request)
|
||||
if err != nil {
|
||||
if errors.Is(err, zerr.ErrUserDataNotFound) {
|
||||
ctlr.Log.Err(err).Msg("can not find user profile in DB")
|
||||
|
||||
authFail(response, request, ctlr.Config.HTTP.Realm, delay)
|
||||
}
|
||||
|
||||
response.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//nolint: contextcheck
|
||||
amw.sessionAuthn(ctlr, next, response, request, delay)
|
||||
if authenticated {
|
||||
next.ServeHTTP(response, request)
|
||||
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// try anonymous auth only if basic auth/session was not given
|
||||
// we want to bypass auth for mgmt route
|
||||
isMgmtRequested := request.RequestURI == constants.FullMgmtPrefix
|
||||
if ctlr.Config.HTTP.AccessControl.AnonymousPolicyExists() || isMgmtRequested {
|
||||
ctx := getReqContextWithAuthorization("", []string{}, request)
|
||||
*request = *request.WithContext(ctx) //nolint:contextcheck
|
||||
|
||||
next.ServeHTTP(response, request)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
authFail(response, request, ctlr.Config.HTTP.Realm, delay)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -443,7 +437,7 @@ func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
|
|||
|
||||
header := request.Header.Get("Authorization")
|
||||
|
||||
if (header == "" || header == "Basic Og==") && isMgmtRequested {
|
||||
if isAuthorizationHeaderEmpty(request) && isMgmtRequested {
|
||||
next.ServeHTTP(response, request)
|
||||
|
||||
return
|
||||
|
@ -490,8 +484,10 @@ func noPasswdAuth(config *config.Config) mux.MiddlewareFunc {
|
|||
}
|
||||
|
||||
ctx := getReqContextWithAuthorization("", []string{}, request)
|
||||
*request = *request.WithContext(ctx) //nolint:contextcheck
|
||||
|
||||
// Process request
|
||||
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
|
||||
next.ServeHTTP(response, request)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -631,15 +627,6 @@ func getReqContextWithAuthorization(username string, groups []string, request *h
|
|||
return ctx
|
||||
}
|
||||
|
||||
func isAPIKeyEnabled(config *config.Config) bool {
|
||||
if config.Extensions != nil && config.Extensions.APIKey != nil &&
|
||||
*config.Extensions.APIKey.Enable {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func authFail(w http.ResponseWriter, r *http.Request, realm string, delay int) {
|
||||
time.Sleep(time.Duration(delay) * time.Second)
|
||||
|
||||
|
@ -658,6 +645,22 @@ func authFail(w http.ResponseWriter, r *http.Request, realm string, delay int) {
|
|||
common.WriteJSON(w, http.StatusUnauthorized, apiErr.NewErrorList(apiErr.NewError(apiErr.UNAUTHORIZED)))
|
||||
}
|
||||
|
||||
func isAuthorizationHeaderEmpty(request *http.Request) bool {
|
||||
header := request.Header.Get("Authorization")
|
||||
|
||||
if header == "" || (strings.ToLower(header) == "basic og==") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func hasSessionHeader(request *http.Request) bool {
|
||||
clientHeader := request.Header.Get(constants.SessionClientHeaderName)
|
||||
|
||||
return clientHeader == constants.SessionClientHeaderValue
|
||||
}
|
||||
|
||||
func getUsernamePasswordBasicAuth(request *http.Request) (string, string, error) {
|
||||
basicAuth := request.Header.Get("Authorization")
|
||||
|
||||
|
@ -800,3 +803,39 @@ func hashUUID(uuid string) string {
|
|||
|
||||
return godigest.NewDigestFromEncoded(godigest.SHA256, fmt.Sprintf("%x", digester.Sum(nil))).Encoded()
|
||||
}
|
||||
|
||||
/*
|
||||
GetAuthUserFromRequestSession returns identity
|
||||
and auth status if on the request's cookie session is a logged in user.
|
||||
*/
|
||||
func GetAuthUserFromRequestSession(cookieStore sessions.Store, request *http.Request, log log.Logger,
|
||||
) (string, bool) {
|
||||
session, err := cookieStore.Get(request, "session")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("can not decode existing session")
|
||||
// expired cookie, no need to return err
|
||||
return "", false
|
||||
}
|
||||
|
||||
// at this point we should have a session set on cookie.
|
||||
// if created in the earlier Get() call then user is not logged in with sessions.
|
||||
if session.IsNew {
|
||||
return "", false
|
||||
}
|
||||
|
||||
authenticated := session.Values["authStatus"]
|
||||
if authenticated != true {
|
||||
log.Error().Msg("can not get `user` session value")
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
identity, ok := session.Values["user"].(string)
|
||||
if !ok {
|
||||
log.Error().Msg("can not get `user` session value")
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
return identity, true
|
||||
}
|
||||
|
|
|
@ -182,8 +182,7 @@ func (c *Controller) Run(reloadCtx context.Context) error {
|
|||
|
||||
if c.Config.HTTP.TLS.CACert != "" {
|
||||
clientAuth := tls.VerifyClientCertIfGiven
|
||||
if (c.Config.HTTP.Auth == nil || c.Config.HTTP.Auth.HTPasswd.Path == "") &&
|
||||
!c.Config.HTTP.AccessControl.AnonymousPolicyExists() {
|
||||
if !c.Config.IsBasicAuthnEnabled() && !c.Config.HTTP.AccessControl.AnonymousPolicyExists() {
|
||||
clientAuth = tls.RequireAndVerifyClientCert
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -8,13 +8,11 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
apiErr "zotregistry.io/zot/pkg/api/errors"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
)
|
||||
|
||||
func AllowedMethods(methods ...string) []string {
|
||||
|
@ -96,39 +94,3 @@ func QueryHasParams(values url.Values, params []string) bool {
|
|||
|
||||
return true
|
||||
}
|
||||
|
||||
/*
|
||||
GetAuthUserFromRequestSession returns identity
|
||||
and auth status if on the request's cookie session is a logged in user.
|
||||
*/
|
||||
func GetAuthUserFromRequestSession(cookieStore sessions.Store, request *http.Request, log log.Logger,
|
||||
) (string, bool) {
|
||||
session, err := cookieStore.Get(request, "session")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("can not decode existing session")
|
||||
// expired cookie, no need to return err
|
||||
return "", false
|
||||
}
|
||||
|
||||
// at this point we should have a session set on cookie.
|
||||
// if created in the earlier Get() call then user is not logged in with sessions.
|
||||
if session.IsNew {
|
||||
return "", false
|
||||
}
|
||||
|
||||
authenticated := session.Values["authStatus"]
|
||||
if authenticated != true {
|
||||
log.Error().Msg("can not get `user` session value")
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
identity, ok := session.Values["user"].(string)
|
||||
if !ok {
|
||||
log.Error().Msg("can not get `user` session value")
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
return identity, true
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue