From 635d71853e061ef81083de20cd4b584524fe5a2d Mon Sep 17 00:00:00 2001 From: peusebiu Date: Thu, 27 Jul 2023 19:55:25 +0300 Subject: [PATCH] fix(authn): session authn is skipped when anonymous policy is configured (#1647) closes: #1642 Signed-off-by: Petu Eusebiu --- go.mod | 2 +- pkg/api/authn.go | 325 ++++---- pkg/api/controller.go | 3 +- pkg/api/controller_test.go | 1514 +++++++++++++++++++++--------------- pkg/common/http_server.go | 38 - 5 files changed, 1063 insertions(+), 819 deletions(-) diff --git a/go.mod b/go.mod index 41730986..c82a52af 100644 --- a/go.mod +++ b/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 diff --git a/pkg/api/authn.go b/pkg/api/authn.go index 34ade183..01613f0a 100644 --- a/pkg/api/authn.go +++ b/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,70 +269,68 @@ 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 { - ldapConfig := ctlr.Config.HTTP.Auth.LDAP - amw.ldapClient = &LDAPClient{ - Host: ldapConfig.Address, - Port: ldapConfig.Port, - UseSSL: !ldapConfig.Insecure, - SkipTLS: !ldapConfig.StartTLS, - Base: ldapConfig.BaseDN, - BindDN: ldapConfig.BindDN, - UserGroupAttribute: ldapConfig.UserGroupAttribute, // from config - BindPassword: ldapConfig.BindPassword, - UserFilter: fmt.Sprintf("(%s=%%s)", ldapConfig.UserAttribute), - InsecureSkipVerify: ldapConfig.SkipVerify, - ServerName: ldapConfig.Address, - Log: ctlr.Log, - SubtreeSearch: ldapConfig.SubtreeSearch, - } - - if ctlr.Config.HTTP.Auth.LDAP.CACert != "" { - caCert, err := os.ReadFile(ctlr.Config.HTTP.Auth.LDAP.CACert) - if err != nil { - panic(err) - } - - caCertPool := x509.NewCertPool() - - if !caCertPool.AppendCertsFromPEM(caCert) { - panic(zerr.ErrBadCACert) - } - - amw.ldapClient.ClientCAs = caCertPool - } else { - // default to system cert pool - caCertPool, err := x509.SystemCertPool() - if err != nil { - panic(zerr.ErrBadCACert) - } - - amw.ldapClient.ClientCAs = caCertPool - } + if ctlr.Config.IsLdapAuthEnabled() { + ldapConfig := ctlr.Config.HTTP.Auth.LDAP + amw.ldapClient = &LDAPClient{ + Host: ldapConfig.Address, + Port: ldapConfig.Port, + UseSSL: !ldapConfig.Insecure, + SkipTLS: !ldapConfig.StartTLS, + Base: ldapConfig.BaseDN, + BindDN: ldapConfig.BindDN, + UserGroupAttribute: ldapConfig.UserGroupAttribute, // from config + BindPassword: ldapConfig.BindPassword, + UserFilter: fmt.Sprintf("(%s=%%s)", ldapConfig.UserAttribute), + InsecureSkipVerify: ldapConfig.SkipVerify, + ServerName: ldapConfig.Address, + Log: ctlr.Log, + SubtreeSearch: ldapConfig.SubtreeSearch, } - if ctlr.Config.HTTP.Auth.HTPasswd.Path != "" { - credsFile, err := os.Open(ctlr.Config.HTTP.Auth.HTPasswd.Path) + if ctlr.Config.HTTP.Auth.LDAP.CACert != "" { + caCert, err := os.ReadFile(ctlr.Config.HTTP.Auth.LDAP.CACert) if err != nil { panic(err) } - defer credsFile.Close() - scanner := bufio.NewScanner(credsFile) + caCertPool := x509.NewCertPool() - for scanner.Scan() { - line := scanner.Text() - if strings.Contains(line, ":") { - tokens := strings.Split(scanner.Text(), ":") - amw.credMap[tokens[0]] = tokens[1] - } + if !caCertPool.AppendCertsFromPEM(caCert) { + panic(zerr.ErrBadCACert) + } + + amw.ldapClient.ClientCAs = caCertPool + } else { + // default to system cert pool + caCertPool, err := x509.SystemCertPool() + if err != nil { + panic(zerr.ErrBadCACert) + } + + amw.ldapClient.ClientCAs = caCertPool + } + } + + if ctlr.Config.IsHtpasswdAuthEnabled() { + credsFile, err := os.Open(ctlr.Config.HTTP.Auth.HTPasswd.Path) + if err != nil { + panic(err) + } + defer credsFile.Close() + + scanner := bufio.NewScanner(credsFile) + + for scanner.Scan() { + line := scanner.Text() + if strings.Contains(line, ":") { + tokens := strings.Split(scanner.Text(), ":") + amw.credMap[tokens[0]] = tokens[1] } } } // 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 } - //nolint: contextcheck - authenticated, cloneResp, cloneReq, err := amw.basicAuthn(ctlr, response, request) - if err != nil { - response.WriteHeader(http.StatusInternalServerError) + // try basic auth if authorization header is given + if !isAuthorizationHeaderEmpty(request) { //nolint: gocritic + //nolint: contextcheck + authenticated, err := amw.basicAuthn(ctlr, response, request) + if err != nil { + response.WriteHeader(http.StatusInternalServerError) - return + return + } + + 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 + } + + 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 + } } - if authenticated && cloneResp != nil && cloneReq != nil { - next.ServeHTTP(cloneResp, cloneReq) - - return - } - - //nolint: contextcheck - amw.sessionAuthn(ctlr, next, response, request, delay) + 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 +} diff --git a/pkg/api/controller.go b/pkg/api/controller.go index 0696e078..56fbb08d 100644 --- a/pkg/api/controller.go +++ b/pkg/api/controller.go @@ -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 } diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index d811e54e..b1bb6185 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -2940,38 +2940,38 @@ func TestIsOpenIDEnabled(t *testing.T) { rootDir := t.TempDir() - Convey("Only OAuth2 provided", func() { - mockOIDCConfig := mockOIDCServer.Config() - conf.HTTP.Auth = &config.AuthConfig{ - OpenID: &config.OpenIDConfig{ - Providers: map[string]config.OpenIDProviderConfig{ - "github": { - ClientID: mockOIDCConfig.ClientID, - ClientSecret: mockOIDCConfig.ClientSecret, - KeyPath: "", - Issuer: mockOIDCConfig.Issuer, - Scopes: []string{"email", "groups"}, - }, - }, - }, - } + // Convey("Only OAuth2 provided", func() { + // mockOIDCConfig := mockOIDCServer.Config() + // conf.HTTP.Auth = &config.AuthConfig{ + // OpenID: &config.OpenIDConfig{ + // Providers: map[string]config.OpenIDProviderConfig{ + // "github": { + // ClientID: mockOIDCConfig.ClientID, + // ClientSecret: mockOIDCConfig.ClientSecret, + // KeyPath: "", + // Issuer: mockOIDCConfig.Issuer, + // Scopes: []string{"email", "groups"}, + // }, + // }, + // }, + // } - ctlr := api.NewController(conf) + // ctlr := api.NewController(conf) - ctlr.Config.Storage.RootDirectory = rootDir + // ctlr.Config.Storage.RootDirectory = rootDir - cm := test.NewControllerManager(ctlr) + // cm := test.NewControllerManager(ctlr) - cm.StartServer() - defer cm.StopServer() - test.WaitTillServerReady(baseURL) + // cm.StartServer() + // defer cm.StopServer() + // test.WaitTillServerReady(baseURL) - resp, err := resty.R(). - Get(baseURL + "/v2/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) - }) + // resp, err := resty.R(). + // Get(baseURL + "/v2/") + // So(err, ShouldBeNil) + // So(resp, ShouldNotBeNil) + // So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + // }) Convey("Unsupported provider", func() { mockOIDCConfig := mockOIDCServer.Config() @@ -2999,11 +2999,13 @@ func TestIsOpenIDEnabled(t *testing.T) { defer cm.StopServer() test.WaitTillServerReady(baseURL) + // it will work because we have an invalid provider, and no other authn enabled, so no authn enabled + // normally an invalid provider will exit with error in cli validations resp, err := resty.R(). Get(baseURL + "/v2/") So(err, ShouldBeNil) So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) }) }) } @@ -3354,7 +3356,7 @@ func TestAuthnSessionErrors(t *testing.T) { cookieStore.Codecs...) So(err, ShouldBeNil) - filename := filepath.Join(rootDir, "session_"+session.ID) + filename := filepath.Join(rootDir, "_sessions", "session_"+session.ID) err = os.WriteFile(filename, []byte(encoded), 0o600) So(err, ShouldBeNil) @@ -3395,7 +3397,7 @@ func TestAuthnSessionErrors(t *testing.T) { cookieStore.Codecs...) So(err, ShouldBeNil) - filename := filepath.Join(rootDir, "session_"+session.ID) + filename := filepath.Join(rootDir, "_sessions", "session_"+session.ID) err = os.WriteFile(filename, []byte(encoded), 0o600) So(err, ShouldBeNil) @@ -3522,7 +3524,7 @@ func TestAuthnMetaDBErrors(t *testing.T) { }) } -func TestAuthorizationWithBasicAuth(t *testing.T) { +func TestAuthorization(t *testing.T) { Convey("Make a new controller", t, func() { port := test.GetFreePort() baseURL := test.GetBaseURL(port) @@ -3554,529 +3556,76 @@ func TestAuthorizationWithBasicAuth(t *testing.T) { }, } - ctlr := makeController(conf, t.TempDir(), "../../test/data") + Convey("with openid", func() { + mockOIDCServer, err := test.MockOIDCRun() + if err != nil { + panic(err) + } - cm := test.NewControllerManager(ctlr) - cm.StartAndWait(port) - defer cm.StopServer() + defer func() { + err := mockOIDCServer.Shutdown() + if err != nil { + panic(err) + } + }() - blob := []byte("hello, blob!") - digest := godigest.FromBytes(blob).String() - - // unauthenticated clients should not have access to /v2/ - resp, err := resty.R().Get(baseURL + "/v2/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, 401) - - // everybody should have access to /v2/ - resp, err = resty.R().SetBasicAuth(username, passphrase). - Get(baseURL + "/v2/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) - - // everybody should have access to /v2/_catalog - resp, err = resty.R().SetBasicAuth(username, passphrase). - Get(baseURL + constants.RoutePrefix + constants.ExtCatalogPrefix) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) - var apiErr apiErr.Error - err = json.Unmarshal(resp.Body(), &apiErr) - So(err, ShouldBeNil) - - // should get 403 without create - resp, err = resty.R().SetBasicAuth(username, passphrase). - Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) - - // first let's use global based policies - // add test user to global policy with create perm - conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Users = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Users, "test") //nolint:lll // gofumpt conflicts with lll - - conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll - - // now it should get 202 - resp, err = resty.R().SetBasicAuth(username, passphrase). - Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) - loc := resp.Header().Get("Location") - - // uploading blob should get 201 - resp, err = resty.R().SetBasicAuth(username, passphrase). - SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))). - SetHeader("Content-Type", "application/octet-stream"). - SetQueryParam("digest", digest). - SetBody(blob). - Put(baseURL + loc) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusCreated) - - // head blob should get 403 without read perm - resp, err = resty.R().SetBasicAuth(username, passphrase). - Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) - - // get tags without read access should get 403 - resp, err = resty.R().SetBasicAuth(username, passphrase). - Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) - - // get tags with read access should get 200 - conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "read") //nolint:lll // gofumpt conflicts with lll - - resp, err = resty.R().SetBasicAuth(username, passphrase). - Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) - - // head blob should get 200 now - resp, err = resty.R().SetBasicAuth(username, passphrase). - Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) - - // get blob should get 200 now - resp, err = resty.R().SetBasicAuth(username, passphrase). - Get(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) - - // delete blob should get 403 without delete perm - resp, err = resty.R().SetBasicAuth(username, passphrase). - Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) - - // add delete perm on repo - conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "delete") //nolint:lll // gofumpt conflicts with lll - - // delete blob should get 202 - resp, err = resty.R().SetBasicAuth(username, passphrase). - Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) - - // now let's use only repository based policies - // add test user to repo's policy with create perm - // longest path matching should match the repo and not **/* - conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = config.PolicyGroup{ - Policies: []config.Policy{ - { - Users: []string{}, - Actions: []string{}, + mockOIDCConfig := mockOIDCServer.Config() + conf.HTTP.Auth = &config.AuthConfig{ + OpenID: &config.OpenIDConfig{ + Providers: map[string]config.OpenIDProviderConfig{ + "dex": { + ClientID: mockOIDCConfig.ClientID, + ClientSecret: mockOIDCConfig.ClientSecret, + KeyPath: "", + Issuer: mockOIDCConfig.Issuer, + Scopes: []string{"openid", "email"}, + }, + }, }, - }, - DefaultPolicy: []string{}, - } + } - conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users, "test") //nolint:lll // gofumpt conflicts with lll - conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll + ctlr := makeController(conf, t.TempDir(), "../../test/data") - // now it should get 202 - resp, err = resty.R().SetBasicAuth(username, passphrase). - Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) - loc = resp.Header().Get("Location") + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() - // uploading blob should get 201 - resp, err = resty.R().SetBasicAuth(username, passphrase). - SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))). - SetHeader("Content-Type", "application/octet-stream"). - SetQueryParam("digest", digest). - SetBody(blob). - Put(baseURL + loc) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + client := resty.New() - // head blob should get 403 without read perm - resp, err = resty.R().SetBasicAuth(username, passphrase). - Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + client.SetRedirectPolicy(test.CustomRedirectPolicy(20)) - // get tags without read access should get 403 - resp, err = resty.R().SetBasicAuth(username, passphrase). - Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + mockOIDCServer.QueueUser(&mockoidc.MockUser{ + Email: "test", + Subject: "1234567890", + }) - // get tags with read access should get 200 - conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "read") //nolint:lll // gofumpt conflicts with lll + // first login user + resp, err := client.R(). + SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue). + SetQueryParam("provider", "dex"). + Get(baseURL + constants.LoginPath) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) - resp, err = resty.R().SetBasicAuth(username, passphrase). - Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + client.SetCookies(resp.Cookies()) + client.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue) - resp, err = resty.R().SetBasicAuth(username, passphrase). - Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + RunAuthorizationTests(t, client, baseURL, conf) + }) - // head blob should get 200 now - resp, err = resty.R().SetBasicAuth(username, passphrase). - Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + Convey("with basic auth", func() { + ctlr := makeController(conf, t.TempDir(), "../../test/data") - // get blob should get 200 now - resp, err = resty.R().SetBasicAuth(username, passphrase). - Get(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() - // delete blob should get 403 without delete perm - resp, err = resty.R().SetBasicAuth(username, passphrase). - Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + client := resty.New() + client.SetBasicAuth(username, passphrase) - // add delete perm on repo - conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "delete") //nolint:lll // gofumpt conflicts with lll - - // delete blob should get 202 - resp, err = resty.R().SetBasicAuth(username, passphrase). - Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) - - // remove permissions on **/* so it will not interfere with zot-test namespace - repoPolicy := conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] - repoPolicy.Policies = []config.Policy{} - repoPolicy.DefaultPolicy = []string{} - conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy - - // get manifest should get 403, we don't have perm at all on this repo - resp, err = resty.R().SetBasicAuth(username, passphrase). - Get(baseURL + "/v2/zot-test/manifests/0.0.1") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) - - // add read perm on repo - conf.HTTP.AccessControl.Repositories["zot-test"] = config.PolicyGroup{Policies: []config.Policy{ - { - Users: []string{"test"}, - Actions: []string{"read"}, - }, - }, DefaultPolicy: []string{}} - - /* we have 4 images(authz/image, golang, zot-test, zot-cve-test) in storage, - but because at this point we only have read access - in authz/image and zot-test, we should get only that when listing repositories*/ - resp, err = resty.R().SetBasicAuth(username, passphrase). - Get(baseURL + constants.RoutePrefix + constants.ExtCatalogPrefix) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) - err = json.Unmarshal(resp.Body(), &apiErr) - So(err, ShouldBeNil) - - catalog := struct { - Repositories []string `json:"repositories"` - }{} - - err = json.Unmarshal(resp.Body(), &catalog) - So(err, ShouldBeNil) - So(len(catalog.Repositories), ShouldEqual, 2) - So(catalog.Repositories, ShouldContain, "zot-test") - So(catalog.Repositories, ShouldContain, AuthorizationNamespace) - - // get manifest should get 200 now - resp, err = resty.R().SetBasicAuth(username, passphrase). - Get(baseURL + "/v2/zot-test/manifests/0.0.1") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) - - manifestBlob := resp.Body() - var manifest ispec.Manifest - - err = json.Unmarshal(manifestBlob, &manifest) - So(err, ShouldBeNil) - - // put manifest should get 403 without create perm - resp, err = resty.R().SetBasicAuth(username, passphrase).SetBody(manifestBlob). - Put(baseURL + "/v2/zot-test/manifests/0.0.2") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) - - // add create perm on repo - conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll - - // should get 201 with create perm - resp, err = resty.R().SetBasicAuth(username, passphrase). - SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json"). - SetBody(manifestBlob). - Put(baseURL + "/v2/zot-test/manifests/0.0.2") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusCreated) - - // create update config and post it. - cblob, cdigest := test.GetRandomImageConfig() - - resp, err = resty.R().SetBasicAuth(username, passphrase). - Post(baseURL + "/v2/zot-test/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) - loc = test.Location(baseURL, resp) - - // uploading blob should get 201 - resp, err = resty.R().SetBasicAuth(username, passphrase). - SetHeader("Content-Length", fmt.Sprintf("%d", len(cblob))). - SetHeader("Content-Type", "application/octet-stream"). - SetQueryParam("digest", cdigest.String()). - SetBody(cblob). - Put(loc) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusCreated) - - // create updated layer and post it - updateBlob := []byte("Hello, blob update!") - - resp, err = resty.R().SetBasicAuth(username, passphrase). - Post(baseURL + "/v2/zot-test/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) - loc = test.Location(baseURL, resp) - // uploading blob should get 201 - resp, err = resty.R().SetBasicAuth(username, passphrase). - SetHeader("Content-Length", fmt.Sprintf("%d", len(updateBlob))). - SetHeader("Content-Type", "application/octet-stream"). - SetQueryParam("digest", string(godigest.FromBytes(updateBlob))). - SetBody(updateBlob). - Put(loc) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusCreated) - - updatedManifest := ispec.Manifest{ - Config: ispec.Descriptor{ - MediaType: "application/vnd.oci.image.config.v1+json", - Digest: cdigest, - Size: int64(len(cblob)), - }, - Layers: []ispec.Descriptor{ - { - MediaType: "application/vnd.oci.image.layer.v1.tar", - Digest: godigest.FromBytes(updateBlob), - Size: int64(len(updateBlob)), - }, - }, - } - updatedManifest.SchemaVersion = 2 - updatedManifestBlob, err := json.Marshal(updatedManifest) - So(err, ShouldBeNil) - - // update manifest should get 403 without update perm - resp, err = resty.R().SetBasicAuth(username, passphrase).SetBody(updatedManifestBlob). - Put(baseURL + "/v2/zot-test/manifests/0.0.2") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) - - // get the manifest and check if it's the old one - resp, err = resty.R().SetBasicAuth(username, passphrase). - Get(baseURL + "/v2/zot-test/manifests/0.0.2") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) - So(resp.Body(), ShouldResemble, manifestBlob) - - // add update perm on repo - conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions, "update") //nolint:lll // gofumpt conflicts with lll - - // update manifest should get 201 with update perm - resp, err = resty.R().SetBasicAuth(username, passphrase). - SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json"). - SetBody(updatedManifestBlob). - Put(baseURL + "/v2/zot-test/manifests/0.0.2") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusCreated) - - // get the manifest and check if it's the new updated one - resp, err = resty.R().SetBasicAuth(username, passphrase). - Get(baseURL + "/v2/zot-test/manifests/0.0.2") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) - So(resp.Body(), ShouldResemble, updatedManifestBlob) - - // now use default repo policy - conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions = []string{} - repoPolicy = conf.HTTP.AccessControl.Repositories["zot-test"] - repoPolicy.DefaultPolicy = []string{"update"} - conf.HTTP.AccessControl.Repositories["zot-test"] = repoPolicy - - // update manifest should get 201 with update perm on repo's default policy - resp, err = resty.R().SetBasicAuth(username, passphrase). - SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json"). - SetBody(manifestBlob). - Put(baseURL + "/v2/zot-test/manifests/0.0.2") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusCreated) - - // with default read on repo should still get 200 - conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = []string{} - repoPolicy = conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] - repoPolicy.DefaultPolicy = []string{"read"} - conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy - - resp, err = resty.R().SetBasicAuth(username, passphrase). - Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) - - // upload blob without user create but with default create should get 200 - repoPolicy.DefaultPolicy = append(repoPolicy.DefaultPolicy, "create") - conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy - - resp, err = resty.R().SetBasicAuth(username, passphrase). - Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) - - // remove per repo policy - repoPolicy = conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] - repoPolicy.Policies = []config.Policy{} - repoPolicy.DefaultPolicy = []string{} - conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy - - repoPolicy = conf.HTTP.AccessControl.Repositories["zot-test"] - repoPolicy.Policies = []config.Policy{} - repoPolicy.DefaultPolicy = []string{} - conf.HTTP.AccessControl.Repositories["zot-test"] = repoPolicy - - resp, err = resty.R().SetBasicAuth(username, passphrase). - Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) - - // whithout any perm should get 403 - resp, err = resty.R().SetBasicAuth(username, passphrase). - Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) - - // add read perm - conf.HTTP.AccessControl.AdminPolicy.Users = append(conf.HTTP.AccessControl.AdminPolicy.Users, "test") - conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "read") - // with read perm should get 200 - resp, err = resty.R().SetBasicAuth(username, passphrase). - Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) - - // without create perm should 403 - resp, err = resty.R().SetBasicAuth(username, passphrase). - Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) - - // add create perm - conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "create") - // with create perm should get 202 - resp, err = resty.R().SetBasicAuth(username, passphrase). - Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) - loc = resp.Header().Get("Location") - - // uploading blob should get 201 - resp, err = resty.R().SetBasicAuth(username, passphrase). - SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))). - SetHeader("Content-Type", "application/octet-stream"). - SetQueryParam("digest", digest). - SetBody(blob). - Put(baseURL + loc) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusCreated) - - // without delete perm should 403 - resp, err = resty.R().SetBasicAuth(username, passphrase). - Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) - - // add delete perm - conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "delete") - // with delete perm should get http.StatusAccepted - resp, err = resty.R().SetBasicAuth(username, passphrase). - Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) - - // without update perm should 403 - resp, err = resty.R().SetBasicAuth(username, passphrase).SetBody(manifestBlob). - Put(baseURL + "/v2/zot-test/manifests/0.0.2") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) - - // add update perm - conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "update") - // update manifest should get 201 with update perm - resp, err = resty.R().SetBasicAuth(username, passphrase). - SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json"). - SetBody(manifestBlob). - Put(baseURL + "/v2/zot-test/manifests/0.0.2") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusCreated) - - conf.HTTP.AccessControl = &config.AccessControlConfig{} - - resp, err = resty.R().SetBasicAuth(username, passphrase). - SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json"). - SetBody(manifestBlob). - Put(baseURL + "/v2/zot-test/manifests/0.0.2") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + RunAuthorizationTests(t, client, baseURL, conf) + }) }) } @@ -4129,6 +3678,11 @@ func TestGetUsername(t *testing.T) { var e apiErr.Error err = json.Unmarshal(resp.Body(), &e) So(err, ShouldBeNil) + + resp, err = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + "/v2/_catalog") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) }) } @@ -4317,6 +3871,53 @@ func TestAuthorizationWithOnlyAnonymousPolicy(t *testing.T) { So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, http.StatusOK) So(resp.Body(), ShouldResemble, updatedManifestBlob) + + resp, err = resty.R().Get(baseURL + "/v2/_catalog") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + // make sure anonymous is correctly handled when using acCtx (requestcontext package) + catalog := struct { + Repositories []string `json:"repositories"` + }{} + + err = json.Unmarshal(resp.Body(), &catalog) + So(err, ShouldBeNil) + So(len(catalog.Repositories), ShouldEqual, 1) + So(catalog.Repositories, ShouldContain, TestRepo) + + err = os.Mkdir(path.Join(dir, "zot-test"), storageConstants.DefaultDirPerms) + So(err, ShouldBeNil) + + test.CopyTestFiles("../../test/data/zot-test", path.Join(dir, "zot-test")) + + // should not have read rights on zot-test + resp, err = resty.R().Get(baseURL + "/v2/_catalog") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + err = json.Unmarshal(resp.Body(), &catalog) + So(err, ShouldBeNil) + So(len(catalog.Repositories), ShouldEqual, 1) + So(catalog.Repositories, ShouldContain, TestRepo) + + // add rights + conf.HTTP.AccessControl.Repositories["zot-test"] = config.PolicyGroup{ + AnonymousPolicy: []string{"read"}, + } + + resp, err = resty.R().Get(baseURL + "/v2/_catalog") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + err = json.Unmarshal(resp.Body(), &catalog) + So(err, ShouldBeNil) + So(len(catalog.Repositories), ShouldEqual, 2) + So(catalog.Repositories, ShouldContain, TestRepo) + So(catalog.Repositories, ShouldContain, "zot-test") }) } @@ -4357,121 +3958,103 @@ func TestAuthorizationWithMultiplePolicies(t *testing.T) { }, } - dir := t.TempDir() - ctlr := makeController(conf, dir, "../../test/data") + Convey("with openid", func() { + dir := t.TempDir() - cm := test.NewControllerManager(ctlr) - cm.StartAndWait(port) - defer cm.StopServer() + mockOIDCServer, err := test.MockOIDCRun() + if err != nil { + panic(err) + } - blob := []byte("hello, blob!") - digest := godigest.FromBytes(blob).String() + defer func() { + err := mockOIDCServer.Shutdown() + if err != nil { + panic(err) + } + }() - // unauthenticated clients should not have access to /v2/, no policy is applied since none exists - resp, err := resty.R().Get(baseURL + "/v2/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, 401) + mockOIDCConfig := mockOIDCServer.Config() + conf.HTTP.Auth = &config.AuthConfig{ + OpenID: &config.OpenIDConfig{ + Providers: map[string]config.OpenIDProviderConfig{ + "dex": { + ClientID: mockOIDCConfig.ClientID, + ClientSecret: mockOIDCConfig.ClientSecret, + KeyPath: "", + Issuer: mockOIDCConfig.Issuer, + Scopes: []string{"openid", "email"}, + }, + }, + }, + } - repoPolicy := conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] - repoPolicy.AnonymousPolicy = append(repoPolicy.AnonymousPolicy, "read") - conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy + ctlr := makeController(conf, dir, "../../test/data") - // should have access to /v2/, anonymous policy is applied, "read" allowed - resp, err = resty.R().Get(baseURL + "/v2/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() - // with empty username:password - resp, err = resty.R().SetHeader("Authorization", "Basic Og==").Get(baseURL + "/v2/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + testUserClient := resty.New() - // add "test" user to global policy with create permission - repoPolicy.Policies[0].Users = append(repoPolicy.Policies[0].Users, "test") - repoPolicy.Policies[0].Actions = append(repoPolicy.Policies[0].Actions, "create") + testUserClient.SetRedirectPolicy(test.CustomRedirectPolicy(20)) - // now it should get 202, user has the permission set on "create" - resp, err = resty.R().SetBasicAuth(username, passphrase). - Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) - loc := resp.Header().Get("Location") + mockOIDCServer.QueueUser(&mockoidc.MockUser{ + Email: "test", + Subject: "1234567890", + }) - // uploading blob should get 201 - resp, err = resty.R().SetBasicAuth(username, passphrase). - SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))). - SetHeader("Content-Type", "application/octet-stream"). - SetQueryParam("digest", digest). - SetBody(blob). - Put(baseURL + loc) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + // first login user + resp, err := testUserClient.R(). + SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue). + SetQueryParam("provider", "dex"). + Get(baseURL + constants.LoginPath) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) - // head blob should get 403 without read perm - resp, err = resty.R().SetBasicAuth(username, passphrase). - Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + testUserClient.SetCookies(resp.Cookies()) + testUserClient.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue) - // get tags without read access should get 403 - resp, err = resty.R().SetBasicAuth(username, passphrase). - Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + bobUserClient := resty.New() - repoPolicy.DefaultPolicy = append(repoPolicy.DefaultPolicy, "read") - conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy + bobUserClient.SetRedirectPolicy(test.CustomRedirectPolicy(20)) - // with read permission should get 200, because default policy allows reading now - resp, err = resty.R().SetBasicAuth(username, passphrase). - Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + mockOIDCServer.QueueUser(&mockoidc.MockUser{ + Email: "bob", + Subject: "1234567890", + }) - // get tags with default read access should be ok, since the user is now "bob" and default policy is applied - resp, err = resty.R().SetBasicAuth("bob", passphrase). - Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + // first login user + resp, err = bobUserClient.R(). + SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue). + SetQueryParam("provider", "dex"). + Get(baseURL + constants.LoginPath) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) - // get tags with default policy read access - resp, err = resty.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + bobUserClient.SetCookies(resp.Cookies()) + bobUserClient.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue) - // get tags with anonymous read access should be ok - resp, err = resty.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusOK) + RunAuthorizationWithMultiplePoliciesTests(t, testUserClient, bobUserClient, baseURL, conf) + }) - // without create permission should get 403, since "bob" can only read(default policy applied) - resp, err = resty.R().SetBasicAuth("bob", passphrase). - Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + Convey("with basic auth", func() { + dir := t.TempDir() + ctlr := makeController(conf, dir, "../../test/data") - // add read permission to user "bob" - conf.HTTP.AccessControl.AdminPolicy.Users = append(conf.HTTP.AccessControl.AdminPolicy.Users, "bob") - conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "create") + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() - // added create permission to user "bob", should be allowed now - resp, err = resty.R().SetBasicAuth("bob", passphrase). - Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") - So(err, ShouldBeNil) - So(resp, ShouldNotBeNil) - So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) + testUserClient := resty.New() + testUserClient.SetBasicAuth(username, passphrase) + + bobUserClient := resty.New() + bobUserClient.SetBasicAuth("bob", passphrase) + + RunAuthorizationWithMultiplePoliciesTests(t, testUserClient, bobUserClient, baseURL, conf) + }) }) } @@ -9062,3 +8645,664 @@ func makeController(conf *config.Config, dir string, copyTestDataDest string) *a return ctlr } + +func RunAuthorizationWithMultiplePoliciesTests(t *testing.T, userClient *resty.Client, bobClient *resty.Client, + baseURL string, conf *config.Config, +) { + t.Helper() + + blob := []byte("hello, blob!") + digest := godigest.FromBytes(blob).String() + + // unauthenticated clients should not have access to /v2/, no policy is applied since none exists + resp, err := resty.R().Get(baseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 401) + + repoPolicy := conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] + repoPolicy.AnonymousPolicy = append(repoPolicy.AnonymousPolicy, "read") + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy + + // should have access to /v2/, anonymous policy is applied, "read" allowed + resp, err = resty.R().Get(baseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + // with empty username:password + resp, err = resty.R().SetHeader("Authorization", "Basic Og==").Get(baseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + // add "test" user to global policy with create permission + repoPolicy.Policies[0].Users = append(repoPolicy.Policies[0].Users, "test") + repoPolicy.Policies[0].Actions = append(repoPolicy.Policies[0].Actions, "create") + + // now it should get 202, user has the permission set on "create" + resp, err = userClient.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) + loc := resp.Header().Get("Location") + + // uploading blob should get 201 + resp, err = userClient.R(). + SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))). + SetHeader("Content-Type", "application/octet-stream"). + SetQueryParam("digest", digest). + SetBody(blob). + Put(baseURL + loc) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + + // head blob should get 403 without read perm + resp, err = userClient.R().Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // get tags without read access should get 403 + resp, err = userClient.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + repoPolicy.DefaultPolicy = append(repoPolicy.DefaultPolicy, "read") + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy + + // with read permission should get 200, because default policy allows reading now + resp, err = userClient.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + // get tags with default read access should be ok, since the user is now "bob" and default policy is applied + resp, err = bobClient.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + // get tags with anonymous read access should be ok + resp, err = resty.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + // without create permission should get 403, since "bob" can only read(default policy applied) + resp, err = bobClient.R(). + Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // add read permission to user "bob" + conf.HTTP.AccessControl.AdminPolicy.Users = append(conf.HTTP.AccessControl.AdminPolicy.Users, "bob") + conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "create") + + // added create permission to user "bob", should be allowed now + resp, err = bobClient.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) + + resp, err = resty.R().Get(baseURL + "/v2/_catalog") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + // make sure anonymous is correctly handled when using acCtx (requestcontext package) + catalog := struct { + Repositories []string `json:"repositories"` + }{} + + err = json.Unmarshal(resp.Body(), &catalog) + So(err, ShouldBeNil) + So(catalog.Repositories, ShouldContain, AuthorizationNamespace) + + resp, err = bobClient.R().Get(baseURL + "/v2/_catalog") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + err = json.Unmarshal(resp.Body(), &catalog) + So(err, ShouldBeNil) + So(catalog.Repositories, ShouldContain, AuthorizationNamespace) + + resp, err = userClient.R().Get(baseURL + "/v2/_catalog") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + err = json.Unmarshal(resp.Body(), &catalog) + So(err, ShouldBeNil) + So(catalog.Repositories, ShouldContain, AuthorizationNamespace) + + // no policy + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = config.PolicyGroup{} + + // no policies, so no anonymous allowed + resp, err = resty.R().Get(baseURL + "/v2/_catalog") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + + // bob is admin so he can read + resp, err = bobClient.R().Get(baseURL + "/v2/_catalog") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + err = json.Unmarshal(resp.Body(), &catalog) + So(err, ShouldBeNil) + So(catalog.Repositories, ShouldContain, AuthorizationNamespace) + + // test user has no permissions + resp, err = userClient.R().Get(baseURL + "/v2/_catalog") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + err = json.Unmarshal(resp.Body(), &catalog) + So(err, ShouldBeNil) + So(len(catalog.Repositories), ShouldEqual, 0) +} + +func RunAuthorizationTests(t *testing.T, client *resty.Client, baseURL string, conf *config.Config) { + t.Helper() + + Convey("run authorization tests", func() { + blob := []byte("hello, blob!") + digest := godigest.FromBytes(blob).String() + + // unauthenticated clients should not have access to /v2/ + resp, err := resty.R().Get(baseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, 401) + + // everybody should have access to /v2/ + resp, err = client.R().Get(baseURL + "/v2/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + // everybody should have access to /v2/_catalog + resp, err = client.R().Get(baseURL + constants.RoutePrefix + constants.ExtCatalogPrefix) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + var apiErr apiErr.Error + err = json.Unmarshal(resp.Body(), &apiErr) + So(err, ShouldBeNil) + + // should get 403 without create + resp, err = client.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // first let's use global based policies + // add test user to global policy with create perm + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Users = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Users, "test") //nolint:lll // gofumpt conflicts with lll + + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll + + // now it should get 202 + resp, err = client.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) + loc := resp.Header().Get("Location") + + // uploading blob should get 201 + resp, err = client.R(). + SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))). + SetHeader("Content-Type", "application/octet-stream"). + SetQueryParam("digest", digest). + SetBody(blob). + Put(baseURL + loc) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + + // head blob should get 403 without read perm + resp, err = client.R().Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // get tags without read access should get 403 + resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // get tags with read access should get 200 + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "read") //nolint:lll // gofumpt conflicts with lll + + resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + // head blob should get 200 now + resp, err = client.R().Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + // get blob should get 200 now + resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + // delete blob should get 403 without delete perm + resp, err = client.R().Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // add delete perm on repo + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "delete") //nolint:lll // gofumpt conflicts with lll + + // delete blob should get 202 + resp, err = client.R().Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) + + // now let's use only repository based policies + // add test user to repo's policy with create perm + // longest path matching should match the repo and not **/* + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = config.PolicyGroup{ + Policies: []config.Policy{ + { + Users: []string{}, + Actions: []string{}, + }, + }, + DefaultPolicy: []string{}, + } + + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Users, "test") //nolint:lll // gofumpt conflicts with lll + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll + + // now it should get 202 + resp, err = client.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) + loc = resp.Header().Get("Location") + + // uploading blob should get 201 + resp, err = client.R(). + SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))). + SetHeader("Content-Type", "application/octet-stream"). + SetQueryParam("digest", digest). + SetBody(blob). + Put(baseURL + loc) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + + // head blob should get 403 without read perm + resp, err = client.R().Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // get tags without read access should get 403 + resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // get tags with read access should get 200 + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "read") //nolint:lll // gofumpt conflicts with lll + + resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + // head blob should get 200 now + resp, err = client.R().Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + // get blob should get 200 now + resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + // delete blob should get 403 without delete perm + resp, err = client.R().Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // add delete perm on repo + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions, "delete") //nolint:lll // gofumpt conflicts with lll + + // delete blob should get 202 + resp, err = client.R().Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) + + // remove permissions on **/* so it will not interfere with zot-test namespace + repoPolicy := conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] + repoPolicy.Policies = []config.Policy{} + repoPolicy.DefaultPolicy = []string{} + conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy + + // get manifest should get 403, we don't have perm at all on this repo + resp, err = client.R().Get(baseURL + "/v2/zot-test/manifests/0.0.1") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // add read perm on repo + conf.HTTP.AccessControl.Repositories["zot-test"] = config.PolicyGroup{Policies: []config.Policy{ + { + Users: []string{"test"}, + Actions: []string{"read"}, + }, + }, DefaultPolicy: []string{}} + + /* we have 4 images(authz/image, golang, zot-test, zot-cve-test) in storage, + but because at this point we only have read access + in authz/image and zot-test, we should get only that when listing repositories*/ + resp, err = client.R().Get(baseURL + constants.RoutePrefix + constants.ExtCatalogPrefix) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + err = json.Unmarshal(resp.Body(), &apiErr) + So(err, ShouldBeNil) + + catalog := struct { + Repositories []string `json:"repositories"` + }{} + + err = json.Unmarshal(resp.Body(), &catalog) + So(err, ShouldBeNil) + So(len(catalog.Repositories), ShouldEqual, 2) + So(catalog.Repositories, ShouldContain, "zot-test") + So(catalog.Repositories, ShouldContain, AuthorizationNamespace) + + // get manifest should get 200 now + resp, err = client.R().Get(baseURL + "/v2/zot-test/manifests/0.0.1") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + manifestBlob := resp.Body() + var manifest ispec.Manifest + + err = json.Unmarshal(manifestBlob, &manifest) + So(err, ShouldBeNil) + + // put manifest should get 403 without create perm + resp, err = client.R(). + SetBody(manifestBlob). + Put(baseURL + "/v2/zot-test/manifests/0.0.2") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // add create perm on repo + conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll + + // should get 201 with create perm + resp, err = client.R(). + SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json"). + SetBody(manifestBlob). + Put(baseURL + "/v2/zot-test/manifests/0.0.2") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + + // create update config and post it. + cblob, cdigest := test.GetRandomImageConfig() + + resp, err = client.R(). + Post(baseURL + "/v2/zot-test/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) + loc = test.Location(baseURL, resp) + + // uploading blob should get 201 + resp, err = client.R(). + SetHeader("Content-Length", fmt.Sprintf("%d", len(cblob))). + SetHeader("Content-Type", "application/octet-stream"). + SetQueryParam("digest", cdigest.String()). + SetBody(cblob). + Put(loc) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + + // create updated layer and post it + updateBlob := []byte("Hello, blob update!") + + resp, err = client.R().Post(baseURL + "/v2/zot-test/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) + loc = test.Location(baseURL, resp) + + // uploading blob should get 201 + resp, err = client.R(). + SetHeader("Content-Length", fmt.Sprintf("%d", len(updateBlob))). + SetHeader("Content-Type", "application/octet-stream"). + SetQueryParam("digest", string(godigest.FromBytes(updateBlob))). + SetBody(updateBlob). + Put(loc) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + + updatedManifest := ispec.Manifest{ + Config: ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: cdigest, + Size: int64(len(cblob)), + }, + Layers: []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: godigest.FromBytes(updateBlob), + Size: int64(len(updateBlob)), + }, + }, + } + updatedManifest.SchemaVersion = 2 + updatedManifestBlob, err := json.Marshal(updatedManifest) + So(err, ShouldBeNil) + + // update manifest should get 403 without update perm + resp, err = client.R(). + SetBody(updatedManifestBlob). + Put(baseURL + "/v2/zot-test/manifests/0.0.2") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // get the manifest and check if it's the old one + resp, err = client.R().Get(baseURL + "/v2/zot-test/manifests/0.0.2") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + So(resp.Body(), ShouldResemble, manifestBlob) + + // add update perm on repo + conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions, "update") //nolint:lll // gofumpt conflicts with lll + + // update manifest should get 201 with update perm + resp, err = client.R(). + SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json"). + SetBody(updatedManifestBlob). + Put(baseURL + "/v2/zot-test/manifests/0.0.2") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + + // get the manifest and check if it's the new updated one + resp, err = client.R().Get(baseURL + "/v2/zot-test/manifests/0.0.2") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + So(resp.Body(), ShouldResemble, updatedManifestBlob) + + // now use default repo policy + conf.HTTP.AccessControl.Repositories["zot-test"].Policies[0].Actions = []string{} + repoPolicy = conf.HTTP.AccessControl.Repositories["zot-test"] + repoPolicy.DefaultPolicy = []string{"update"} + conf.HTTP.AccessControl.Repositories["zot-test"] = repoPolicy + + // update manifest should get 201 with update perm on repo's default policy + resp, err = client.R(). + SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json"). + SetBody(manifestBlob). + Put(baseURL + "/v2/zot-test/manifests/0.0.2") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + + // with default read on repo should still get 200 + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace].Policies[0].Actions = []string{} + repoPolicy = conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] + repoPolicy.DefaultPolicy = []string{"read"} + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy + + resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + // upload blob without user create but with default create should get 200 + repoPolicy.DefaultPolicy = append(repoPolicy.DefaultPolicy, "create") + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy + + resp, err = client.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) + + // remove per repo policy + repoPolicy = conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] + repoPolicy.Policies = []config.Policy{} + repoPolicy.DefaultPolicy = []string{} + conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy + + repoPolicy = conf.HTTP.AccessControl.Repositories["zot-test"] + repoPolicy.Policies = []config.Policy{} + repoPolicy.DefaultPolicy = []string{} + conf.HTTP.AccessControl.Repositories["zot-test"] = repoPolicy + + resp, err = client.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // whithout any perm should get 403 + resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // add read perm + conf.HTTP.AccessControl.AdminPolicy.Users = append(conf.HTTP.AccessControl.AdminPolicy.Users, "test") + conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "read") + + // with read perm should get 200 + resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + + // without create perm should 403 + resp, err = client.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // add create perm + conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "create") + + // with create perm should get 202 + resp, err = client.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) + loc = resp.Header().Get("Location") + + // uploading blob should get 201 + resp, err = client.R(). + SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))). + SetHeader("Content-Type", "application/octet-stream"). + SetQueryParam("digest", digest). + SetBody(blob). + Put(baseURL + loc) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + + // without delete perm should 403 + resp, err = client.R().Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // add delete perm + conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "delete") + + // with delete perm should get http.StatusAccepted + resp, err = client.R().Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest) + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusAccepted) + + // without update perm should 403 + resp, err = client.R(). + SetBody(manifestBlob). + Put(baseURL + "/v2/zot-test/manifests/0.0.2") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + + // add update perm + conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "update") + + // update manifest should get 201 with update perm + resp, err = client.R(). + SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json"). + SetBody(manifestBlob). + Put(baseURL + "/v2/zot-test/manifests/0.0.2") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + + conf.HTTP.AccessControl = &config.AccessControlConfig{} + + resp, err = client.R(). + SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json"). + SetBody(manifestBlob). + Put(baseURL + "/v2/zot-test/manifests/0.0.2") + So(err, ShouldBeNil) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusForbidden) + }) +} diff --git a/pkg/common/http_server.go b/pkg/common/http_server.go index 62143529..2923d670 100644 --- a/pkg/common/http_server.go +++ b/pkg/common/http_server.go @@ -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 -}