mirror of
https://github.com/project-zot/zot.git
synced 2026-06-16 04:17:55 +08:00
refactor(authz): use a struct for user access control info operations (#1682)
fix(authz): fix isAdmin not using groups to determine if a user is admin. fix(authz): return 401 instead of 403 403 is correct as per HTTP spec However authz is not part of dist-spec and clients know only about 401 So this is a compromise. Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
This commit is contained in:
+64
-48
@@ -38,7 +38,7 @@ import (
|
||||
apiErr "zotregistry.io/zot/pkg/api/errors"
|
||||
zcommon "zotregistry.io/zot/pkg/common"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
localCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
reqCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
storageConstants "zotregistry.io/zot/pkg/storage/constants"
|
||||
)
|
||||
|
||||
@@ -63,8 +63,8 @@ func AuthHandler(ctlr *Controller) mux.MiddlewareFunc {
|
||||
return authnMiddleware.TryAuthnHandlers(ctlr)
|
||||
}
|
||||
|
||||
func (amw *AuthnMiddleware) sessionAuthn(ctlr *Controller, response http.ResponseWriter,
|
||||
request *http.Request,
|
||||
func (amw *AuthnMiddleware) sessionAuthn(ctlr *Controller, userAc *reqCtx.UserAccessControl,
|
||||
response http.ResponseWriter, request *http.Request,
|
||||
) (bool, error) {
|
||||
identity, ok := GetAuthUserFromRequestSession(ctlr.CookieStore, request, ctlr.Log)
|
||||
if !ok {
|
||||
@@ -83,23 +83,24 @@ func (amw *AuthnMiddleware) sessionAuthn(ctlr *Controller, response http.Respons
|
||||
return false, nil
|
||||
}
|
||||
|
||||
ctx := getReqContextWithAuthorization(identity, []string{}, request)
|
||||
userAc.SetUsername(identity)
|
||||
userAc.SaveOnRequest(request)
|
||||
|
||||
groups, err := ctlr.MetaDB.GetUserGroups(ctx)
|
||||
groups, err := ctlr.MetaDB.GetUserGroups(request.Context())
|
||||
if err != nil {
|
||||
ctlr.Log.Err(err).Str("identity", identity).Msg("can not get user profile in DB")
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
ctx = getReqContextWithAuthorization(identity, groups, request)
|
||||
*request = *request.WithContext(ctx)
|
||||
userAc.AddGroups(groups)
|
||||
userAc.SaveOnRequest(request)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (amw *AuthnMiddleware) basicAuthn(ctlr *Controller, response http.ResponseWriter,
|
||||
request *http.Request,
|
||||
func (amw *AuthnMiddleware) basicAuthn(ctlr *Controller, userAc *reqCtx.UserAccessControl,
|
||||
response http.ResponseWriter, request *http.Request,
|
||||
) (bool, error) {
|
||||
cookieStore := ctlr.CookieStore
|
||||
|
||||
@@ -122,15 +123,17 @@ func (amw *AuthnMiddleware) basicAuthn(ctlr *Controller, response http.ResponseW
|
||||
groups = ac.getUserGroups(identity)
|
||||
}
|
||||
|
||||
ctx := getReqContextWithAuthorization(identity, groups, request)
|
||||
*request = *request.WithContext(ctx)
|
||||
userAc.SetUsername(identity)
|
||||
userAc.AddGroups(groups)
|
||||
userAc.SaveOnRequest(request)
|
||||
|
||||
// saved logged session
|
||||
if err := saveUserLoggedSession(cookieStore, response, request, identity, ctlr.Log); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := ctlr.MetaDB.SetUserGroups(ctx, groups); err != nil {
|
||||
// we have already populated the request context with userAc
|
||||
if err := ctlr.MetaDB.SetUserGroups(request.Context(), groups); err != nil {
|
||||
ctlr.Log.Error().Err(err).Str("identity", identity).Msg("couldn't update user profile")
|
||||
|
||||
return false, err
|
||||
@@ -156,14 +159,16 @@ func (amw *AuthnMiddleware) basicAuthn(ctlr *Controller, response http.ResponseW
|
||||
|
||||
groups = append(groups, ldapgroups...)
|
||||
|
||||
ctx := getReqContextWithAuthorization(identity, groups, request)
|
||||
*request = *request.WithContext(ctx)
|
||||
userAc.SetUsername(identity)
|
||||
userAc.AddGroups(groups)
|
||||
userAc.SaveOnRequest(request)
|
||||
|
||||
if err := saveUserLoggedSession(cookieStore, response, request, identity, ctlr.Log); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := ctlr.MetaDB.SetUserGroups(ctx, groups); err != nil {
|
||||
// we have already populated the request context with userAc
|
||||
if err := ctlr.MetaDB.SetUserGroups(request.Context(), groups); err != nil {
|
||||
ctlr.Log.Error().Err(err).Str("identity", identity).Msg("couldn't update user profile")
|
||||
|
||||
return false, err
|
||||
@@ -201,10 +206,11 @@ func (amw *AuthnMiddleware) basicAuthn(ctlr *Controller, response http.ResponseW
|
||||
}
|
||||
|
||||
if storedIdentity == identity {
|
||||
ctx := getReqContextWithAuthorization(identity, []string{}, request)
|
||||
userAc.SetUsername(identity)
|
||||
userAc.SaveOnRequest(request)
|
||||
|
||||
// check if api key expired
|
||||
isExpired, err := ctlr.MetaDB.IsAPIKeyExpired(ctx, hashedKey)
|
||||
isExpired, err := ctlr.MetaDB.IsAPIKeyExpired(request.Context(), hashedKey)
|
||||
if err != nil {
|
||||
ctlr.Log.Err(err).Str("identity", identity).Msg("can not verify if api key expired")
|
||||
|
||||
@@ -215,22 +221,22 @@ func (amw *AuthnMiddleware) basicAuthn(ctlr *Controller, response http.ResponseW
|
||||
return false, nil
|
||||
}
|
||||
|
||||
err = ctlr.MetaDB.UpdateUserAPIKeyLastUsed(ctx, hashedKey)
|
||||
err = ctlr.MetaDB.UpdateUserAPIKeyLastUsed(request.Context(), hashedKey)
|
||||
if err != nil {
|
||||
ctlr.Log.Err(err).Str("identity", identity).Msg("can not update user profile in DB")
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
groups, err := ctlr.MetaDB.GetUserGroups(ctx)
|
||||
groups, err := ctlr.MetaDB.GetUserGroups(request.Context())
|
||||
if err != nil {
|
||||
ctlr.Log.Err(err).Str("identity", identity).Msg("can not get user's groups in DB")
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
ctx = getReqContextWithAuthorization(identity, groups, request)
|
||||
*request = *request.WithContext(ctx)
|
||||
userAc.AddGroups(groups)
|
||||
userAc.SaveOnRequest(request)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -242,7 +248,7 @@ func (amw *AuthnMiddleware) basicAuthn(ctlr *Controller, response http.ResponseW
|
||||
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.Config)
|
||||
return noPasswdAuth(ctlr)
|
||||
}
|
||||
|
||||
amw.credMap = make(map[string]string)
|
||||
@@ -369,10 +375,15 @@ func (amw *AuthnMiddleware) TryAuthnHandlers(ctlr *Controller) mux.MiddlewareFun
|
||||
isMgmtRequested := request.RequestURI == constants.FullMgmt
|
||||
allowAnonymous := ctlr.Config.HTTP.AccessControl.AnonymousPolicyExists()
|
||||
|
||||
// build user access control info
|
||||
userAc := reqCtx.NewUserAccessControl()
|
||||
// if it will not be populated by authn handlers, this represents an anonymous user
|
||||
userAc.SaveOnRequest(request)
|
||||
|
||||
// try basic auth if authorization header is given
|
||||
if !isAuthorizationHeaderEmpty(request) { //nolint: gocritic
|
||||
//nolint: contextcheck
|
||||
authenticated, err := amw.basicAuthn(ctlr, response, request)
|
||||
authenticated, err := amw.basicAuthn(ctlr, userAc, response, request)
|
||||
if err != nil {
|
||||
response.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
@@ -387,7 +398,7 @@ func (amw *AuthnMiddleware) TryAuthnHandlers(ctlr *Controller) mux.MiddlewareFun
|
||||
} else if hasSessionHeader(request) {
|
||||
// try session auth
|
||||
//nolint: contextcheck
|
||||
authenticated, err := amw.sessionAuthn(ctlr, response, request)
|
||||
authenticated, err := amw.sessionAuthn(ctlr, userAc, response, request)
|
||||
if err != nil {
|
||||
if errors.Is(err, zerr.ErrUserDataNotFound) {
|
||||
ctlr.Log.Err(err).Msg("can not find user profile in DB")
|
||||
@@ -408,19 +419,12 @@ func (amw *AuthnMiddleware) TryAuthnHandlers(ctlr *Controller) mux.MiddlewareFun
|
||||
|
||||
// the session header can be present also for anonymous calls
|
||||
if allowAnonymous || isMgmtRequested {
|
||||
ctx := getReqContextWithAuthorization("", []string{}, request)
|
||||
*request = *request.WithContext(ctx) //nolint:contextcheck
|
||||
|
||||
next.ServeHTTP(response, request)
|
||||
|
||||
return
|
||||
}
|
||||
} else if allowAnonymous || isMgmtRequested {
|
||||
// try anonymous auth only if basic auth/session was not given
|
||||
// we want to bypass auth for mgmt route
|
||||
ctx := getReqContextWithAuthorization("", []string{}, request)
|
||||
*request = *request.WithContext(ctx) //nolint:contextcheck
|
||||
|
||||
next.ServeHTTP(response, request)
|
||||
|
||||
return
|
||||
@@ -495,7 +499,7 @@ func bearerAuthHandler(ctlr *Controller) mux.MiddlewareFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func noPasswdAuth(config *config.Config) mux.MiddlewareFunc {
|
||||
func noPasswdAuth(ctlr *Controller) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
if request.Method == http.MethodOptions {
|
||||
@@ -505,8 +509,29 @@ func noPasswdAuth(config *config.Config) mux.MiddlewareFunc {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := getReqContextWithAuthorization("", []string{}, request)
|
||||
*request = *request.WithContext(ctx) //nolint:contextcheck
|
||||
userAc := reqCtx.NewUserAccessControl()
|
||||
|
||||
// if no basic auth enabled then try to get identity from mTLS auth
|
||||
if request.TLS != nil {
|
||||
verifiedChains := request.TLS.VerifiedChains
|
||||
if len(verifiedChains) > 0 && len(verifiedChains[0]) > 0 {
|
||||
for _, cert := range request.TLS.PeerCertificates {
|
||||
identity := cert.Subject.CommonName
|
||||
if identity != "" {
|
||||
// assign identity to authz context, needed for extensions
|
||||
userAc.SetUsername(identity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ctlr.Config.IsMTLSAuthEnabled() && userAc.IsAnonymous() {
|
||||
authFail(response, request, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
userAc.SaveOnRequest(request)
|
||||
|
||||
// Process request
|
||||
next.ServeHTTP(response, request)
|
||||
@@ -649,18 +674,6 @@ func getRelyingPartyArgs(cfg *config.Config, provider string) (
|
||||
return issuer, clientID, clientSecret, redirectURI, scopes, options
|
||||
}
|
||||
|
||||
func getReqContextWithAuthorization(username string, groups []string, request *http.Request) context.Context {
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: username,
|
||||
Groups: groups,
|
||||
}
|
||||
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(request.Context(), authzCtxKey, acCtx)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func authFail(w http.ResponseWriter, r *http.Request, realm string, delay int) {
|
||||
time.Sleep(time.Duration(delay) * time.Second)
|
||||
|
||||
@@ -809,7 +822,10 @@ func OAuth2Callback(ctlr *Controller, w http.ResponseWriter, r *http.Request, st
|
||||
return "", zerr.ErrInvalidStateCookie
|
||||
}
|
||||
|
||||
ctx := getReqContextWithAuthorization(email, groups, r)
|
||||
userAc := reqCtx.NewUserAccessControl()
|
||||
userAc.SetUsername(email)
|
||||
userAc.AddGroups(groups)
|
||||
userAc.SaveOnRequest(r)
|
||||
|
||||
// if this line has been reached, then a new session should be created
|
||||
// if the `session` key is already on the cookie, it's not a valid one
|
||||
@@ -817,7 +833,7 @@ func OAuth2Callback(ctlr *Controller, w http.ResponseWriter, r *http.Request, st
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := ctlr.MetaDB.SetUserGroups(ctx, groups); err != nil {
|
||||
if err := ctlr.MetaDB.SetUserGroups(r.Context(), groups); err != nil {
|
||||
ctlr.Log.Error().Err(err).Str("identity", email).Msg("couldn't update the user profile")
|
||||
|
||||
return "", err
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
extconf "zotregistry.io/zot/pkg/extensions/config"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
mTypes "zotregistry.io/zot/pkg/meta/types"
|
||||
localCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
reqCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
"zotregistry.io/zot/pkg/test"
|
||||
"zotregistry.io/zot/pkg/test/mocks"
|
||||
)
|
||||
@@ -470,13 +470,9 @@ func TestAPIKeys(t *testing.T) {
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: email,
|
||||
}
|
||||
|
||||
ctx := context.WithValue(context.Background(), authzCtxKey, acCtx)
|
||||
userAc := reqCtx.NewUserAccessControl()
|
||||
userAc.SetUsername(email)
|
||||
ctx := userAc.DeriveContext(context.Background())
|
||||
|
||||
err = ctlr.MetaDB.DeleteUserData(ctx)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
+54
-96
@@ -8,22 +8,16 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
"zotregistry.io/zot/pkg/common"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
localCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
reqCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
)
|
||||
|
||||
const (
|
||||
// method actions.
|
||||
Create = "create"
|
||||
Read = "read"
|
||||
Update = "update"
|
||||
Delete = "delete"
|
||||
// behaviour actions.
|
||||
DetectManifestCollision = "detectManifestCollision"
|
||||
BASIC = "Basic"
|
||||
BEARER = "Bearer"
|
||||
OPENID = "OpenID"
|
||||
BASIC = "Basic"
|
||||
BEARER = "Bearer"
|
||||
OPENID = "OpenID"
|
||||
)
|
||||
|
||||
// AccessController authorizes users to act on resources.
|
||||
@@ -90,7 +84,7 @@ func (ac *AccessController) getGlobPatterns(username string, groups []string, ac
|
||||
}
|
||||
|
||||
// can verifies if a user can do action on repository.
|
||||
func (ac *AccessController) can(ctx context.Context, username, action, repository string) bool {
|
||||
func (ac *AccessController) can(userAc *reqCtx.UserAccessControl, action, repository string) bool {
|
||||
can := false
|
||||
|
||||
var longestMatchedPattern string
|
||||
@@ -104,12 +98,8 @@ func (ac *AccessController) can(ctx context.Context, username, action, repositor
|
||||
}
|
||||
}
|
||||
|
||||
acCtx, err := localCtx.GetAccessControlContext(ctx)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
userGroups := acCtx.Groups
|
||||
userGroups := userAc.GetGroups()
|
||||
username := userAc.GetUsername()
|
||||
|
||||
// check matched repo based policy
|
||||
pg, ok := ac.Config.Repositories[longestMatchedPattern]
|
||||
@@ -119,11 +109,7 @@ func (ac *AccessController) can(ctx context.Context, username, action, repositor
|
||||
|
||||
// check admins based policy
|
||||
if !can {
|
||||
if ac.isAdmin(username) && common.Contains(ac.Config.AdminPolicy.Actions, action) {
|
||||
can = true
|
||||
}
|
||||
|
||||
if ac.isAnyGroupInAdminPolicy(userGroups) && common.Contains(ac.Config.AdminPolicy.Actions, action) {
|
||||
if ac.isAdmin(username, userGroups) && common.Contains(ac.Config.AdminPolicy.Actions, action) {
|
||||
can = true
|
||||
}
|
||||
}
|
||||
@@ -132,8 +118,12 @@ func (ac *AccessController) can(ctx context.Context, username, action, repositor
|
||||
}
|
||||
|
||||
// isAdmin .
|
||||
func (ac *AccessController) isAdmin(username string) bool {
|
||||
return common.Contains(ac.Config.AdminPolicy.Users, username)
|
||||
func (ac *AccessController) isAdmin(username string, userGroups []string) bool {
|
||||
if common.Contains(ac.Config.AdminPolicy.Users, username) || ac.isAnyGroupInAdminPolicy(userGroups) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (ac *AccessController) isAnyGroupInAdminPolicy(userGroups []string) bool {
|
||||
@@ -161,33 +151,37 @@ func (ac *AccessController) getUserGroups(username string) []string {
|
||||
return groupNames
|
||||
}
|
||||
|
||||
// getContext updates an AccessControlContext for a user/anonymous and returns a context.Context containing it.
|
||||
func (ac *AccessController) getContext(acCtx *localCtx.AccessControlContext, request *http.Request) context.Context {
|
||||
readGlobPatterns := ac.getGlobPatterns(acCtx.Username, acCtx.Groups, Read)
|
||||
dmcGlobPatterns := ac.getGlobPatterns(acCtx.Username, acCtx.Groups, DetectManifestCollision)
|
||||
// getContext updates an UserAccessControl with admin status and specific permissions on repos.
|
||||
func (ac *AccessController) updateUserAccessControl(userAc *reqCtx.UserAccessControl) {
|
||||
identity := userAc.GetUsername()
|
||||
groups := userAc.GetGroups()
|
||||
|
||||
acCtx.ReadGlobPatterns = readGlobPatterns
|
||||
acCtx.DmcGlobPatterns = dmcGlobPatterns
|
||||
readGlobPatterns := ac.getGlobPatterns(identity, groups, constants.ReadPermission)
|
||||
createGlobPatterns := ac.getGlobPatterns(identity, groups, constants.CreatePermission)
|
||||
updateGlobPatterns := ac.getGlobPatterns(identity, groups, constants.UpdatePermission)
|
||||
deleteGlobPatterns := ac.getGlobPatterns(identity, groups, constants.DeletePermission)
|
||||
dmcGlobPatterns := ac.getGlobPatterns(identity, groups, constants.DetectManifestCollisionPermission)
|
||||
|
||||
if ac.isAdmin(acCtx.Username) {
|
||||
acCtx.IsAdmin = true
|
||||
userAc.SetGlobPatterns(constants.ReadPermission, readGlobPatterns)
|
||||
userAc.SetGlobPatterns(constants.CreatePermission, createGlobPatterns)
|
||||
userAc.SetGlobPatterns(constants.UpdatePermission, updateGlobPatterns)
|
||||
userAc.SetGlobPatterns(constants.DeletePermission, deleteGlobPatterns)
|
||||
userAc.SetGlobPatterns(constants.DetectManifestCollisionPermission, dmcGlobPatterns)
|
||||
|
||||
if ac.isAdmin(userAc.GetUsername(), userAc.GetGroups()) {
|
||||
userAc.SetIsAdmin(true)
|
||||
} else {
|
||||
acCtx.IsAdmin = false
|
||||
userAc.SetIsAdmin(false)
|
||||
}
|
||||
|
||||
authzCtxKey := localCtx.GetContextKey()
|
||||
ctx := context.WithValue(request.Context(), authzCtxKey, *acCtx)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
// getAuthnMiddlewareContext builds ac context(allowed to read repos and if user is admin) and returns it.
|
||||
func (ac *AccessController) getAuthnMiddlewareContext(authnType string, request *http.Request) context.Context {
|
||||
amwCtx := localCtx.AuthnMiddlewareContext{
|
||||
amwCtx := reqCtx.AuthnMiddlewareContext{
|
||||
AuthnType: authnType,
|
||||
}
|
||||
|
||||
amwCtxKey := localCtx.GetAuthnMiddlewareCtxKey()
|
||||
amwCtxKey := reqCtx.GetAuthnMiddlewareCtxKey()
|
||||
ctx := context.WithValue(request.Context(), amwCtxKey, amwCtx)
|
||||
|
||||
return ctx
|
||||
@@ -254,7 +248,7 @@ func BaseAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
||||
}
|
||||
|
||||
// request comes from bearer authn, bypass it
|
||||
authnMwCtx, err := localCtx.GetAuthnMiddlewareContext(request.Context())
|
||||
authnMwCtx, err := reqCtx.GetAuthnMiddlewareContext(request.Context())
|
||||
if err != nil || (authnMwCtx != nil && authnMwCtx.AuthnType == BEARER) {
|
||||
next.ServeHTTP(response, request)
|
||||
|
||||
@@ -268,51 +262,20 @@ func BaseAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
||||
return
|
||||
}
|
||||
|
||||
acCtrlr := NewAccessController(ctlr.Config)
|
||||
aCtlr := NewAccessController(ctlr.Config)
|
||||
|
||||
var identity string
|
||||
// get access control context made in authn.go
|
||||
userAc, err := reqCtx.UserAcFromContext(request.Context())
|
||||
if err != nil { // should never happen
|
||||
authFail(response, request, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
|
||||
|
||||
// anonymous context
|
||||
acCtx := &localCtx.AccessControlContext{}
|
||||
|
||||
// get username from context made in authn.go
|
||||
if ctlr.Config.IsBasicAuthnEnabled() {
|
||||
// get access control context made in authn.go if authn is enabled
|
||||
acCtx, err = localCtx.GetAccessControlContext(request.Context())
|
||||
if err != nil { // should never happen
|
||||
authFail(response, request, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
identity = acCtx.Username
|
||||
return
|
||||
}
|
||||
|
||||
if request.TLS != nil {
|
||||
verifiedChains := request.TLS.VerifiedChains
|
||||
// still no identity, get it from TLS certs
|
||||
if identity == "" && verifiedChains != nil &&
|
||||
len(verifiedChains) > 0 && len(verifiedChains[0]) > 0 {
|
||||
for _, cert := range request.TLS.PeerCertificates {
|
||||
identity = cert.Subject.CommonName
|
||||
}
|
||||
aCtlr.updateUserAccessControl(userAc)
|
||||
userAc.SaveOnRequest(request)
|
||||
|
||||
// if we still don't have an identity
|
||||
if identity == "" {
|
||||
acCtrlr.Log.Info().Msg("couldn't get identity from TLS certificate")
|
||||
authFail(response, request, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// assign identity to authz context, needed for extensions
|
||||
acCtx.Username = identity
|
||||
}
|
||||
}
|
||||
|
||||
ctx := acCtrlr.getContext(acCtx, request)
|
||||
|
||||
next.ServeHTTP(response, request.WithContext(ctx)) //nolint:contextcheck
|
||||
next.ServeHTTP(response, request) //nolint:contextcheck
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -327,7 +290,7 @@ func DistSpecAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
||||
}
|
||||
|
||||
// request comes from bearer authn, bypass it
|
||||
authnMwCtx, err := localCtx.GetAuthnMiddlewareContext(request.Context())
|
||||
authnMwCtx, err := reqCtx.GetAuthnMiddlewareContext(request.Context())
|
||||
if err != nil || (authnMwCtx != nil && authnMwCtx.AuthnType == BEARER) {
|
||||
next.ServeHTTP(response, request)
|
||||
|
||||
@@ -340,45 +303,40 @@ func DistSpecAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
|
||||
|
||||
acCtrlr := NewAccessController(ctlr.Config)
|
||||
|
||||
var identity string
|
||||
|
||||
// get acCtx built in authn and previous authz middlewares
|
||||
acCtx, err := localCtx.GetAccessControlContext(request.Context())
|
||||
// get userAc built in authn and previous authz middlewares
|
||||
userAc, err := reqCtx.UserAcFromContext(request.Context())
|
||||
if err != nil { // should never happen
|
||||
authFail(response, request, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// get username from context made in authn.go
|
||||
identity = acCtx.Username
|
||||
|
||||
var action string
|
||||
if request.Method == http.MethodGet || request.Method == http.MethodHead {
|
||||
action = Read
|
||||
action = constants.ReadPermission
|
||||
}
|
||||
|
||||
if request.Method == http.MethodPut || request.Method == http.MethodPatch || request.Method == http.MethodPost {
|
||||
// assume user wants to create
|
||||
action = Create
|
||||
action = constants.CreatePermission
|
||||
// if we get a reference (tag)
|
||||
if ok {
|
||||
is := ctlr.StoreController.GetImageStore(resource)
|
||||
tags, err := is.GetImageTags(resource)
|
||||
// if repo exists and request's tag exists then action is UPDATE
|
||||
if err == nil && common.Contains(tags, reference) && reference != "latest" {
|
||||
action = Update
|
||||
action = constants.UpdatePermission
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if request.Method == http.MethodDelete {
|
||||
action = Delete
|
||||
action = constants.DeletePermission
|
||||
}
|
||||
|
||||
can := acCtrlr.can(request.Context(), identity, action, resource) //nolint:contextcheck
|
||||
can := acCtrlr.can(userAc, action, resource) //nolint:contextcheck
|
||||
if !can {
|
||||
common.AuthzFail(response, request, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
|
||||
common.AuthzFail(response, request, userAc.GetUsername(), ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
|
||||
} else {
|
||||
next.ServeHTTP(response, request) //nolint:contextcheck
|
||||
}
|
||||
|
||||
@@ -249,6 +249,19 @@ func (c *Config) IsLdapAuthEnabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Config) IsMTLSAuthEnabled() bool {
|
||||
if c.HTTP.TLS != nil &&
|
||||
c.HTTP.TLS.Key != "" &&
|
||||
c.HTTP.TLS.Cert != "" &&
|
||||
c.HTTP.TLS.CACert != "" &&
|
||||
!c.IsBasicAuthnEnabled() &&
|
||||
!c.HTTP.AccessControl.AnonymousPolicyExists() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Config) IsHtpasswdAuthEnabled() bool {
|
||||
if c.HTTP.Auth != nil && c.HTTP.Auth.HTPasswd.Path != "" {
|
||||
return true
|
||||
|
||||
@@ -23,4 +23,12 @@ const (
|
||||
APIKeysPrefix = "zak_"
|
||||
CallbackUIQueryParam = "callback_ui"
|
||||
APIKeyTimeFormat = time.RFC3339
|
||||
// authz permissions.
|
||||
// method actions.
|
||||
CreatePermission = "create"
|
||||
ReadPermission = "read"
|
||||
UpdatePermission = "update"
|
||||
DeletePermission = "delete"
|
||||
// behaviour actions.
|
||||
DetectManifestCollisionPermission = "detectManifestCollision"
|
||||
)
|
||||
|
||||
@@ -182,7 +182,7 @@ func (c *Controller) Run(reloadCtx context.Context) error {
|
||||
|
||||
if c.Config.HTTP.TLS.CACert != "" {
|
||||
clientAuth := tls.VerifyClientCertIfGiven
|
||||
if !c.Config.IsBasicAuthnEnabled() && !c.Config.HTTP.AccessControl.AnonymousPolicyExists() {
|
||||
if c.Config.IsMTLSAuthEnabled() {
|
||||
clientAuth = tls.RequireAndVerifyClientCert
|
||||
}
|
||||
|
||||
|
||||
+13
-10
@@ -1449,7 +1449,7 @@ func TestTLSWithBasicAuthAllowReadAccess(t *testing.T) {
|
||||
// without creds, writes should fail
|
||||
resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1552,8 +1552,6 @@ func TestMutualTLSAuthWithoutCN(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
htpasswdPath := test.MakeHtpasswdFile()
|
||||
defer os.Remove(htpasswdPath)
|
||||
|
||||
port := test.GetFreePort()
|
||||
secureBaseURL := test.GetSecureBaseURL(port)
|
||||
@@ -1721,7 +1719,7 @@ func TestTLSMutualAuthAllowReadAccess(t *testing.T) {
|
||||
// without creds, writes should fail
|
||||
resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
|
||||
// setup TLS mutual auth
|
||||
cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key")
|
||||
@@ -1899,7 +1897,7 @@ func TestTLSMutualAndBasicAuthAllowReadAccess(t *testing.T) {
|
||||
// with only client certs, writes should fail
|
||||
resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
|
||||
// with client certs and creds, should get expected status code
|
||||
resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
|
||||
@@ -3755,11 +3753,11 @@ func TestAuthorizationWithOnlyAnonymousPolicy(t *testing.T) {
|
||||
err = json.Unmarshal(resp.Body(), &e)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// should get 403 without create
|
||||
// should get 401 without create
|
||||
resp, err = resty.R().Post(baseURL + "/v2/" + TestRepo + "/blobs/uploads/")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
|
||||
if entry, ok := conf.HTTP.AccessControl.Repositories[TestRepo]; ok {
|
||||
entry.AnonymousPolicy = []string{"create", "read"}
|
||||
@@ -3864,12 +3862,12 @@ func TestAuthorizationWithOnlyAnonymousPolicy(t *testing.T) {
|
||||
updatedManifestBlob, err := json.Marshal(updatedManifest)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// update manifest should get 403 without update perm
|
||||
// update manifest should get 401 without update perm
|
||||
resp, err = resty.R().SetBody(updatedManifestBlob).
|
||||
Put(baseURL + "/v2/" + TestRepo + "/manifests/0.0.2")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
|
||||
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
|
||||
|
||||
// get the manifest and check if it's the old one
|
||||
resp, err = resty.R().
|
||||
@@ -6928,7 +6926,12 @@ func TestManifestCollision(t *testing.T) {
|
||||
conf.HTTP.AccessControl = &config.AccessControlConfig{
|
||||
Repositories: config.Repositories{
|
||||
AuthorizationAllRepos: config.PolicyGroup{
|
||||
AnonymousPolicy: []string{api.Read, api.Create, api.Delete, api.DetectManifestCollision},
|
||||
AnonymousPolicy: []string{
|
||||
constants.ReadPermission,
|
||||
constants.CreatePermission,
|
||||
constants.DeletePermission,
|
||||
constants.DetectManifestCollisionPermission,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+9
-8
@@ -45,7 +45,7 @@ import (
|
||||
"zotregistry.io/zot/pkg/meta"
|
||||
mTypes "zotregistry.io/zot/pkg/meta/types"
|
||||
zreg "zotregistry.io/zot/pkg/regexp"
|
||||
localCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
reqCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
storageCommon "zotregistry.io/zot/pkg/storage/common"
|
||||
storageTypes "zotregistry.io/zot/pkg/storage/types"
|
||||
"zotregistry.io/zot/pkg/test/inject"
|
||||
@@ -777,8 +777,8 @@ func (rh *RouteHandler) DeleteManifest(response http.ResponseWriter, request *ht
|
||||
return
|
||||
}
|
||||
|
||||
// authz request context (set in authz middleware)
|
||||
acCtx, err := localCtx.GetAccessControlContext(request.Context())
|
||||
// user authz request context (set in authz middleware)
|
||||
userAc, err := reqCtx.UserAcFromContext(request.Context())
|
||||
if err != nil {
|
||||
response.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
@@ -786,8 +786,8 @@ func (rh *RouteHandler) DeleteManifest(response http.ResponseWriter, request *ht
|
||||
}
|
||||
|
||||
var detectCollision bool
|
||||
if acCtx != nil {
|
||||
detectCollision = acCtx.CanDetectManifestCollision(name)
|
||||
if userAc != nil {
|
||||
detectCollision = userAc.Can(constants.DetectManifestCollisionPermission, name)
|
||||
}
|
||||
|
||||
manifestBlob, manifestDigest, mediaType, err := imgStore.GetImageManifest(name, reference)
|
||||
@@ -1151,6 +1151,7 @@ func (rh *RouteHandler) DeleteBlob(response http.ResponseWriter, request *http.R
|
||||
// @Success 202 {string} string "accepted"
|
||||
// @Header 202 {string} Location "/v2/{name}/blobs/uploads/{session_id}"
|
||||
// @Header 202 {string} Range "0-0"
|
||||
// @Failure 401 {string} string "unauthorized"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "internal server error"
|
||||
// @Router /v2/{name}/blobs/uploads [post].
|
||||
@@ -1711,16 +1712,16 @@ func (rh *RouteHandler) ListRepositories(response http.ResponseWriter, request *
|
||||
|
||||
repos := make([]string, 0)
|
||||
// authz context
|
||||
acCtx, err := localCtx.GetAccessControlContext(request.Context())
|
||||
userAc, err := reqCtx.UserAcFromContext(request.Context())
|
||||
if err != nil {
|
||||
response.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if acCtx != nil {
|
||||
if userAc != nil {
|
||||
for _, r := range combineRepoList {
|
||||
if acCtx.IsAdmin || acCtx.CanReadRepo(r) {
|
||||
if userAc.Can(constants.ReadPermission, r) {
|
||||
repos = append(repos, r)
|
||||
}
|
||||
}
|
||||
|
||||
+16
-31
@@ -29,7 +29,7 @@ import (
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
mTypes "zotregistry.io/zot/pkg/meta/types"
|
||||
localCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
reqCtx "zotregistry.io/zot/pkg/requestcontext"
|
||||
storageTypes "zotregistry.io/zot/pkg/storage/types"
|
||||
"zotregistry.io/zot/pkg/test"
|
||||
"zotregistry.io/zot/pkg/test/mocks"
|
||||
@@ -141,9 +141,8 @@ func TestRoutes(t *testing.T) {
|
||||
Convey("List repositories authz error", func() {
|
||||
var invalid struct{}
|
||||
|
||||
ctx := context.TODO()
|
||||
key := localCtx.GetContextKey()
|
||||
ctx = context.WithValue(ctx, key, invalid)
|
||||
uacKey := reqCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), uacKey, invalid)
|
||||
|
||||
request, _ := http.NewRequestWithContext(ctx, http.MethodGet, baseURL, nil)
|
||||
request = mux.SetURLVars(request, map[string]string{
|
||||
@@ -164,9 +163,8 @@ func TestRoutes(t *testing.T) {
|
||||
Convey("Delete manifest authz error", func() {
|
||||
var invalid struct{}
|
||||
|
||||
ctx := context.TODO()
|
||||
key := localCtx.GetContextKey()
|
||||
ctx = context.WithValue(ctx, key, invalid)
|
||||
uacKey := reqCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), uacKey, invalid)
|
||||
|
||||
request, _ := http.NewRequestWithContext(ctx, http.MethodGet, baseURL, nil)
|
||||
request = mux.SetURLVars(request, map[string]string{
|
||||
@@ -1419,9 +1417,8 @@ func TestRoutes(t *testing.T) {
|
||||
Convey("CreateAPIKey invalid access control context", func() {
|
||||
var invalid struct{}
|
||||
|
||||
ctx := context.TODO()
|
||||
key := localCtx.GetContextKey()
|
||||
ctx = context.WithValue(ctx, key, invalid)
|
||||
uacKey := reqCtx.GetContextKey()
|
||||
ctx := context.WithValue(context.Background(), uacKey, invalid)
|
||||
|
||||
request, _ := http.NewRequestWithContext(ctx, http.MethodPost, baseURL, bytes.NewReader([]byte{}))
|
||||
response := httptest.NewRecorder()
|
||||
@@ -1443,13 +1440,9 @@ func TestRoutes(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("CreateAPIKey bad request body", func() {
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: "test",
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
key := localCtx.GetContextKey()
|
||||
ctx = context.WithValue(ctx, key, acCtx)
|
||||
userAc := reqCtx.NewUserAccessControl()
|
||||
userAc.SetUsername("test")
|
||||
ctx := userAc.DeriveContext(context.Background())
|
||||
|
||||
request, _ := http.NewRequestWithContext(ctx, http.MethodPost, baseURL, bytes.NewReader([]byte{}))
|
||||
response := httptest.NewRecorder()
|
||||
@@ -1462,13 +1455,9 @@ func TestRoutes(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("CreateAPIKey error on AddUserAPIKey", func() {
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: "test",
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
key := localCtx.GetContextKey()
|
||||
ctx = context.WithValue(ctx, key, acCtx)
|
||||
userAc := reqCtx.NewUserAccessControl()
|
||||
userAc.SetUsername("test")
|
||||
ctx := userAc.DeriveContext(context.Background())
|
||||
|
||||
payload := api.APIKeyPayload{
|
||||
Label: "test",
|
||||
@@ -1494,13 +1483,9 @@ func TestRoutes(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Revoke error on DeleteUserAPIKeyFn", func() {
|
||||
acCtx := localCtx.AccessControlContext{
|
||||
Username: "test",
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
key := localCtx.GetContextKey()
|
||||
ctx = context.WithValue(ctx, key, acCtx)
|
||||
userAc := reqCtx.NewUserAccessControl()
|
||||
userAc.SetUsername("test")
|
||||
ctx := userAc.DeriveContext(context.Background())
|
||||
|
||||
request, _ := http.NewRequestWithContext(ctx, http.MethodDelete, baseURL, bytes.NewReader([]byte{}))
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
Reference in New Issue
Block a user