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:
peusebiu
2023-09-01 21:13:53 +03:00
committed by GitHub
parent b80deb9927
commit c6b822f3dd
28 changed files with 1052 additions and 889 deletions
+64 -48
View File
@@ -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
+4 -8
View File
@@ -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
View File
@@ -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
}
+13
View File
@@ -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
+8
View File
@@ -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"
)
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()