diff --git a/examples/config-metrics.json b/examples/config-metrics.json
index 7ec5845c16..2b94e25835 100644
--- a/examples/config-metrics.json
+++ b/examples/config-metrics.json
@@ -5,7 +5,12 @@
},
"http": {
"address": "127.0.0.1",
- "port": "8080"
+ "port": "8080",
+ "auth": {
+ "htpasswd": {
+ "path": "test/data/htpasswd"
+ }
+ }
},
"log": {
"level": "debug"
diff --git a/pkg/api/authn.go b/pkg/api/authn.go
index 8a8b428496..675c1fc1d0 100644
--- a/pkg/api/authn.go
+++ b/pkg/api/authn.go
@@ -60,7 +60,7 @@ func AuthHandler(ctlr *Controller) mux.MiddlewareFunc {
return bearerAuthHandler(ctlr)
}
- return authnMiddleware.TryAuthnHandlers(ctlr)
+ return authnMiddleware.tryAuthnHandlers(ctlr)
}
func (amw *AuthnMiddleware) sessionAuthn(ctlr *Controller, userAc *reqCtx.UserAccessControl,
@@ -250,7 +250,7 @@ func (amw *AuthnMiddleware) basicAuthn(ctlr *Controller, userAc *reqCtx.UserAcce
return false, nil
}
-func (amw *AuthnMiddleware) TryAuthnHandlers(ctlr *Controller) mux.MiddlewareFunc { //nolint: gocyclo
+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)
diff --git a/pkg/api/authn_test.go b/pkg/api/authn_test.go
index b989d7865d..8062243681 100644
--- a/pkg/api/authn_test.go
+++ b/pkg/api/authn_test.go
@@ -1,37 +1,52 @@
-//go:build mgmt
-// +build mgmt
+//go:build sync && scrub && metrics && search && lint && userprefs && mgmt && imagetrust && ui
+// +build sync,scrub,metrics,search,lint,userprefs,mgmt,imagetrust,ui
package api_test
import (
"context"
+ "crypto/tls"
+ "crypto/x509"
"encoding/json"
- "errors"
+ goerrors "errors"
"fmt"
+ "net"
"net/http"
"net/http/httptest"
+ "net/url"
"os"
+ "path/filepath"
+ "strconv"
+ "strings"
"testing"
"time"
guuid "github.com/gofrs/uuid"
+ "github.com/gorilla/securecookie"
+ "github.com/gorilla/sessions"
+ godigest "github.com/opencontainers/go-digest"
"github.com/project-zot/mockoidc"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/resty.v1"
+ "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
+ apiErr "zotregistry.io/zot/pkg/api/errors"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types"
reqCtx "zotregistry.io/zot/pkg/requestcontext"
+ storageConstants "zotregistry.io/zot/pkg/storage/constants"
authutils "zotregistry.io/zot/pkg/test/auth"
test "zotregistry.io/zot/pkg/test/common"
+ "zotregistry.io/zot/pkg/test/deprecated"
+ . "zotregistry.io/zot/pkg/test/image-utils"
"zotregistry.io/zot/pkg/test/mocks"
)
-var ErrUnexpectedError = errors.New("error: unexpected error")
+var ErrUnexpectedError = goerrors.New("error: unexpected error")
type (
apiKeyResponse struct {
@@ -46,239 +61,2699 @@ type (
}
)
-func TestAllowedMethodsHeaderAPIKey(t *testing.T) {
- defaultVal := true
-
- Convey("Test http options response", t, func() {
- conf := config.New()
+func TestHtpasswdSingleCred(t *testing.T) {
+ Convey("Single cred", t, func() {
port := test.GetFreePort()
- conf.HTTP.Port = port
- conf.HTTP.Auth.APIKey = defaultVal
baseURL := test.GetBaseURL(port)
+ singleCredtests := []string{}
+ user := ALICE
+ password := ALICE
+ singleCredtests = append(singleCredtests, getCredString(user, password))
+ singleCredtests = append(singleCredtests, getCredString(user, password)+"\n")
+
+ for _, testString := range singleCredtests {
+ func() {
+ conf := config.New()
+ conf.HTTP.Port = port
+
+ htpasswdPath := test.MakeHtpasswdFileFromString(testString)
+ defer os.Remove(htpasswdPath)
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ }
- ctlr := api.NewController(conf)
- ctlr.Config.Storage.RootDirectory = t.TempDir()
+ conf.HTTP.AllowOrigin = conf.HTTP.Address
- ctrlManager := test.NewControllerManager(ctlr)
+ ctlr := makeController(conf, t.TempDir())
- ctrlManager.StartAndWait(port)
- defer ctrlManager.StopServer()
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- resp, _ := resty.R().Options(baseURL + constants.APIKeyPath)
- So(resp, ShouldNotBeNil)
- So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,POST,DELETE,OPTIONS")
- So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
+ // with creds, should get expected status code
+ resp, _ := resty.R().SetBasicAuth(user, password).Get(baseURL + "/v2/")
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ header := []string{"Authorization,content-type," + constants.SessionClientHeaderName}
+
+ resp, _ = resty.R().SetBasicAuth(user, password).Options(baseURL + "/v2/")
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
+ So(len(resp.Header()), ShouldEqual, 5)
+ So(resp.Header()["Access-Control-Allow-Headers"], ShouldResemble, header)
+ So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
+
+ // with invalid creds, it should fail
+ resp, _ = resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/")
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ }()
+ }
})
}
-func TestAPIKeys(t *testing.T) {
- Convey("Make a new controller", t, func() {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
+func TestHtpasswdTwoCreds(t *testing.T) {
+ Convey("Two creds", t, func() {
+ twoCredTests := []string{}
+ user1 := "alicia"
+ password1 := "aliciapassword"
+ user2 := "bob"
+ password2 := "robert"
+ twoCredTests = append(twoCredTests, getCredString(user1, password1)+"\n"+
+ getCredString(user2, password2))
+
+ twoCredTests = append(twoCredTests, getCredString(user1, password1)+"\n"+
+ getCredString(user2, password2)+"\n")
+
+ twoCredTests = append(twoCredTests, getCredString(user1, password1)+"\n\n"+
+ getCredString(user2, password2)+"\n\n")
+
+ for _, testString := range twoCredTests {
+ func() {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ conf := config.New()
+ conf.HTTP.Port = port
+ htpasswdPath := test.MakeHtpasswdFileFromString(testString)
+ defer os.Remove(htpasswdPath)
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ }
+ ctlr := makeController(conf, t.TempDir())
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ // with creds, should get expected status code
+ resp, _ := resty.R().SetBasicAuth(user1, password1).Get(baseURL + "/v2/")
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ resp, _ = resty.R().SetBasicAuth(user2, password2).Get(baseURL + "/v2/")
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // with invalid creds, it should fail
+ resp, _ = resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/")
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ }()
+ }
+ })
+}
- conf := config.New()
- conf.HTTP.Port = port
+func TestHtpasswdFiveCreds(t *testing.T) {
+ Convey("Five creds", t, func() {
+ tests := map[string]string{
+ "michael": "scott",
+ "jim": "halpert",
+ "dwight": "shrute",
+ "pam": "bessley",
+ "creed": "bratton",
+ }
+ credString := strings.Builder{}
+ for key, val := range tests {
+ credString.WriteString(getCredString(key, val) + "\n")
+ }
- htpasswdPath := test.MakeHtpasswdFile()
- defer os.Remove(htpasswdPath)
+ func() {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ conf := config.New()
+ conf.HTTP.Port = port
+ htpasswdPath := test.MakeHtpasswdFileFromString(credString.String())
+ defer os.Remove(htpasswdPath)
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ }
+ ctlr := makeController(conf, t.TempDir())
- mockOIDCServer, err := authutils.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)
+ // with creds, should get expected status code
+ for key, val := range tests {
+ resp, _ := resty.R().SetBasicAuth(key, val).Get(baseURL + "/v2/")
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
}
+
+ // with invalid creds, it should fail
+ resp, _ := resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/")
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
}()
+ })
+}
- mockOIDCConfig := mockOIDCServer.Config()
- defaultVal := true
+func TestAllowMethodsHeader(t *testing.T) {
+ Convey("Options request", t, func() {
+ dir := t.TempDir()
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ conf := config.New()
+ conf.HTTP.Port = port
+ conf.Storage.RootDirectory = dir
+ conf.HTTP.AllowOrigin = "someOrigin"
+
+ simpleUser := "simpleUser"
+ simpleUserPassword := "simpleUserPass"
+ credTests := fmt.Sprintf("%s\n\n", getCredString(simpleUser, simpleUserPassword))
+
+ htpasswdPath := test.MakeHtpasswdFileFromString(credTests)
+ defer os.Remove(htpasswdPath)
conf.HTTP.Auth = &config.AuthConfig{
HTPasswd: config.AuthHTPasswd{
Path: htpasswdPath,
},
- OpenID: &config.OpenIDConfig{
- Providers: map[string]config.OpenIDProviderConfig{
- "oidc": {
- ClientID: mockOIDCConfig.ClientID,
- ClientSecret: mockOIDCConfig.ClientSecret,
- KeyPath: "",
- Issuer: mockOIDCConfig.Issuer,
- Scopes: []string{"openid", "email", "groups"},
+ }
+
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Repositories: config.Repositories{
+ "**": config.PolicyGroup{
+ Policies: []config.Policy{
+ {
+ Users: []string{simpleUser},
+ Actions: []string{"read", "create"},
+ },
},
},
},
- APIKey: defaultVal,
}
- conf.HTTP.AccessControl = &config.AccessControlConfig{}
-
- conf.Extensions = &extconf.ExtensionConfig{}
- conf.Extensions.Search = &extconf.SearchConfig{}
- conf.Extensions.Search.Enable = &defaultVal
- conf.Extensions.Search.CVE = nil
- conf.Extensions.UI = &extconf.UIConfig{}
- conf.Extensions.UI.Enable = &defaultVal
+ defaultVal := true
+ conf.Extensions = &extconf.ExtensionConfig{
+ Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
+ }
ctlr := api.NewController(conf)
- dir := t.TempDir()
- ctlr.Config.Storage.RootDirectory = dir
+ ctlrManager := test.NewControllerManager(ctlr)
+ ctlrManager.StartAndWait(port)
+ defer ctlrManager.StopServer()
- cm := test.NewControllerManager(ctlr)
+ simpleUserClient := resty.R().SetBasicAuth(simpleUser, simpleUserPassword)
- cm.StartServer()
- defer cm.StopServer()
- test.WaitTillServerReady(baseURL)
+ digest := godigest.FromString("digest")
- payload := api.APIKeyPayload{
- Label: "test",
- Scopes: []string{"test"},
- }
- reqBody, err := json.Marshal(payload)
+ // /v2
+ resp, err := simpleUserClient.Options(baseURL + "/v2/")
So(err, ShouldBeNil)
+ So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
- Convey("API key retrieved with basic auth", func() {
- resp, err := resty.R().
- SetBody(reqBody).
- SetBasicAuth("test", "test").
- Post(baseURL + constants.APIKeyPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ // /v2/{name}/tags/list
+ resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/tags/list")
+ So(err, ShouldBeNil)
+ So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
- user := mockoidc.DefaultUser()
+ // /v2/{name}/manifests/{reference}
+ resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/manifests/" + digest.String())
+ So(err, ShouldBeNil)
+ So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,OPTIONS")
- // get API key and email from apikey route response
- var apiKeyResponse apiKeyResponse
- err = json.Unmarshal(resp.Body(), &apiKeyResponse)
- So(err, ShouldBeNil)
+ // /v2/{name}/referrers/{digest}
+ resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/referrers/" + digest.String())
+ So(err, ShouldBeNil)
+ So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
- email := user.Email
- So(email, ShouldNotBeEmpty)
+ // /v2/_catalog
+ resp, err = simpleUserClient.Options(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
- resp, err = resty.R().
- SetBasicAuth("test", apiKeyResponse.APIKey).
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ // /v2/_oci/ext/discover
+ resp, err = simpleUserClient.Options(baseURL + "/v2/_oci/ext/discover")
+ So(err, ShouldBeNil)
+ So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
+ })
+}
- // get API key list with basic auth
- resp, err = resty.R().
- SetBasicAuth("test", "test").
- Get(baseURL + constants.APIKeyPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+func TestTLSWithBasicAuth(t *testing.T) {
+ Convey("Make a new controller", t, func() {
+ caCert, err := os.ReadFile(CACert)
+ So(err, ShouldBeNil)
+ caCertPool := x509.NewCertPool()
+ caCertPool.AppendCertsFromPEM(caCert)
+ htpasswdPath := test.MakeHtpasswdFile()
+ defer os.Remove(htpasswdPath)
- var apiKeyListResponse apiKeyListResponse
- err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
- So(err, ShouldBeNil)
- So(len(apiKeyListResponse.APIKeys), ShouldEqual, 1)
- So(apiKeyListResponse.APIKeys[0].CreatedAt, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatedAt)
- So(apiKeyListResponse.APIKeys[0].CreatorUA, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatorUA)
- So(apiKeyListResponse.APIKeys[0].Label, ShouldEqual, apiKeyResponse.APIKeyDetails.Label)
- So(apiKeyListResponse.APIKeys[0].Scopes, ShouldEqual, apiKeyResponse.APIKeyDetails.Scopes)
- So(apiKeyListResponse.APIKeys[0].UUID, ShouldEqual, apiKeyResponse.APIKeyDetails.UUID)
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ secureBaseURL := test.GetSecureBaseURL(port)
- // add another one
- resp, err = resty.R().
- SetBody(reqBody).
- SetBasicAuth("test", "test").
- Post(baseURL + constants.APIKeyPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
+ defer func() { resty.SetTLSClientConfig(nil) }()
+ conf := config.New()
+ conf.HTTP.Port = port
+ conf.HTTP.TLS = &config.TLSConfig{
+ Cert: ServerCert,
+ Key: ServerKey,
+ }
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ }
- err = json.Unmarshal(resp.Body(), &apiKeyResponse)
- So(err, ShouldBeNil)
+ ctlr := makeController(conf, t.TempDir())
- resp, err = resty.R().
- SetBasicAuth("test", apiKeyResponse.APIKey).
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- // get API key list with api key auth
- resp, err = resty.R().
- SetBasicAuth("test", apiKeyResponse.APIKey).
- Get(baseURL + constants.APIKeyPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ // accessing insecure HTTP site should fail
+ resp, err := resty.R().Get(baseURL)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
- err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
- So(err, ShouldBeNil)
- So(len(apiKeyListResponse.APIKeys), ShouldEqual, 2)
- })
+ // without creds, should get access error
+ resp, err = resty.R().Get(secureBaseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ var e apiErr.Error
+ err = json.Unmarshal(resp.Body(), &e)
+ So(err, ShouldBeNil)
- Convey("API key retrieved with openID and with no expire", func() {
- client := resty.New()
- client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+ // with creds, should get expected status code
+ resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- // first login user
- resp, err := client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- SetQueryParam("provider", "oidc").
- Get(baseURL + constants.LoginPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
+ resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ })
+}
- cookies := resp.Cookies()
+func TestTLSWithBasicAuthAllowReadAccess(t *testing.T) {
+ Convey("Make a new controller", t, func() {
+ caCert, err := os.ReadFile(CACert)
+ So(err, ShouldBeNil)
+ caCertPool := x509.NewCertPool()
+ caCertPool.AppendCertsFromPEM(caCert)
+ htpasswdPath := test.MakeHtpasswdFile()
+ defer os.Remove(htpasswdPath)
- // call endpoint without session
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Post(baseURL + constants.APIKeyPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ secureBaseURL := test.GetSecureBaseURL(port)
- client.SetCookies(cookies)
+ resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
+ defer func() { resty.SetTLSClientConfig(nil) }()
+ conf := config.New()
+ conf.HTTP.Port = port
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ }
+ conf.HTTP.TLS = &config.TLSConfig{
+ Cert: ServerCert,
+ Key: ServerKey,
+ }
- // call endpoint with session ( added to client after previous request)
- resp, err = client.R().
- SetBody(reqBody).
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Post(baseURL + constants.APIKeyPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Repositories: config.Repositories{
+ AuthorizationAllRepos: config.PolicyGroup{
+ AnonymousPolicy: []string{"read"},
+ },
+ },
+ }
- user := mockoidc.DefaultUser()
+ ctlr := makeController(conf, t.TempDir())
- // get API key and email from apikey route response
- var apiKeyResponse apiKeyResponse
- err = json.Unmarshal(resp.Body(), &apiKeyResponse)
- So(err, ShouldBeNil)
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- email := user.Email
- So(email, ShouldNotBeEmpty)
+ // accessing insecure HTTP site should fail
+ resp, err := resty.R().Get(baseURL)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
- // get API key list
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Get(baseURL + constants.APIKeyPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ // without creds, should still be allowed to access
+ resp, err = resty.R().Get(secureBaseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- var apiKeyListResponse apiKeyListResponse
+ // with creds, should get expected status code
+ resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
- So(err, ShouldBeNil)
- So(len(apiKeyListResponse.APIKeys), ShouldEqual, 1)
+ resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // without creds, writes should fail
+ resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ })
+}
+
+func TestMutualTLSAuthWithUserPermissions(t *testing.T) {
+ Convey("Make a new controller", t, func() {
+ caCert, err := os.ReadFile(CACert)
+ So(err, ShouldBeNil)
+ caCertPool := x509.NewCertPool()
+ caCertPool.AppendCertsFromPEM(caCert)
+ htpasswdPath := test.MakeHtpasswdFile()
+ defer os.Remove(htpasswdPath)
+
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ secureBaseURL := test.GetSecureBaseURL(port)
+
+ resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
+ defer func() { resty.SetTLSClientConfig(nil) }()
+ conf := config.New()
+ conf.HTTP.Port = port
+
+ conf.HTTP.TLS = &config.TLSConfig{
+ Cert: ServerCert,
+ Key: ServerKey,
+ CACert: CACert,
+ }
+
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Repositories: config.Repositories{
+ AuthorizationAllRepos: config.PolicyGroup{
+ Policies: []config.Policy{
+ {
+ Users: []string{"*"},
+ Actions: []string{"read"},
+ },
+ },
+ },
+ },
+ }
+
+ ctlr := makeController(conf, t.TempDir())
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ resp, err := resty.R().Get(baseURL)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+
+ repoPolicy := conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos]
+
+ // setup TLS mutual auth
+ cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key")
+ So(err, ShouldBeNil)
+
+ resty.SetCertificates(cert)
+ defer func() { resty.SetCertificates(tls.Certificate{}) }()
+
+ // with client certs but without creds, should succeed
+ resp, err = resty.R().Get(secureBaseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ resp, err = resty.R().Get(secureBaseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // with creds, should get expected status code
+ resp, _ = resty.R().Get(secureBaseURL)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+
+ // reading a repo should not get 403
+ resp, err = resty.R().Get(secureBaseURL + "/v2/repo/tags/list")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+
+ // without creds, writes should fail
+ resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
+
+ // empty default authorization and give user the permission to create
+ repoPolicy.Policies[0].Actions = append(repoPolicy.Policies[0].Actions, "create")
+ conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy
+ resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
+ })
+}
+
+func TestMutualTLSAuthWithoutCN(t *testing.T) {
+ Convey("Make a new controller", t, func() {
+ caCert, err := os.ReadFile("../../test/data/noidentity/ca.crt")
+ So(err, ShouldBeNil)
+ caCertPool := x509.NewCertPool()
+ caCertPool.AppendCertsFromPEM(caCert)
+
+ port := test.GetFreePort()
+ secureBaseURL := test.GetSecureBaseURL(port)
+
+ resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
+ defer func() { resty.SetTLSClientConfig(nil) }()
+ conf := config.New()
+ conf.HTTP.Port = port
+
+ conf.HTTP.TLS = &config.TLSConfig{
+ Cert: "../../test/data/noidentity/server.cert",
+ Key: "../../test/data/noidentity/server.key",
+ CACert: "../../test/data/noidentity/ca.crt",
+ }
+
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Repositories: config.Repositories{
+ AuthorizationAllRepos: config.PolicyGroup{
+ Policies: []config.Policy{
+ {
+ Users: []string{"*"},
+ Actions: []string{"read"},
+ },
+ },
+ },
+ },
+ }
+
+ ctlr := makeController(conf, t.TempDir())
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ // setup TLS mutual auth
+ cert, err := tls.LoadX509KeyPair("../../test/data/noidentity/client.cert", "../../test/data/noidentity/client.key")
+ So(err, ShouldBeNil)
+
+ resty.SetCertificates(cert)
+ defer func() { resty.SetCertificates(tls.Certificate{}) }()
+
+ // with client certs but without TLS mutual auth setup should get certificate error
+ resp, _ := resty.R().Get(secureBaseURL + "/v2/_catalog")
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ })
+}
+
+func TestTLSMutualAuth(t *testing.T) {
+ Convey("Make a new controller", t, func() {
+ caCert, err := os.ReadFile(CACert)
+ So(err, ShouldBeNil)
+ caCertPool := x509.NewCertPool()
+ caCertPool.AppendCertsFromPEM(caCert)
+
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ secureBaseURL := test.GetSecureBaseURL(port)
+
+ resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
+ defer func() { resty.SetTLSClientConfig(nil) }()
+ conf := config.New()
+ conf.HTTP.Port = port
+ conf.HTTP.TLS = &config.TLSConfig{
+ Cert: ServerCert,
+ Key: ServerKey,
+ CACert: CACert,
+ }
+
+ ctlr := makeController(conf, t.TempDir())
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ // accessing insecure HTTP site should fail
+ resp, err := resty.R().Get(baseURL)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+
+ // without client certs and creds, should get conn error
+ _, err = resty.R().Get(secureBaseURL)
+ So(err, ShouldNotBeNil)
+
+ // with creds but without certs, should get conn error
+ _, err = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
+ So(err, ShouldNotBeNil)
+
+ // setup TLS mutual auth
+ cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key")
+ So(err, ShouldBeNil)
+
+ resty.SetCertificates(cert)
+ defer func() { resty.SetCertificates(tls.Certificate{}) }()
+
+ // with client certs but without creds, should succeed
+ resp, err = resty.R().Get(secureBaseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // with client certs and creds, should get expected status code
+ resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+
+ // with client certs, creds shouldn't matter
+ resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ })
+}
+
+func TestTLSMutualAuthAllowReadAccess(t *testing.T) {
+ Convey("Make a new controller", t, func() {
+ caCert, err := os.ReadFile(CACert)
+ So(err, ShouldBeNil)
+ caCertPool := x509.NewCertPool()
+ caCertPool.AppendCertsFromPEM(caCert)
+
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ secureBaseURL := test.GetSecureBaseURL(port)
+
+ resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
+ defer func() { resty.SetTLSClientConfig(nil) }()
+ conf := config.New()
+ conf.HTTP.Port = port
+ conf.HTTP.TLS = &config.TLSConfig{
+ Cert: ServerCert,
+ Key: ServerKey,
+ CACert: CACert,
+ }
+
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Repositories: config.Repositories{
+ AuthorizationAllRepos: config.PolicyGroup{
+ AnonymousPolicy: []string{"read"},
+ },
+ },
+ }
+
+ ctlr := makeController(conf, t.TempDir())
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ // accessing insecure HTTP site should fail
+ resp, err := resty.R().Get(baseURL)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+
+ // without client certs and creds, reads are allowed
+ resp, err = resty.R().Get(secureBaseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // with creds but without certs, reads are allowed
+ resp, err = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // without creds, writes should fail
+ resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ // setup TLS mutual auth
+ cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key")
+ So(err, ShouldBeNil)
+
+ resty.SetCertificates(cert)
+ defer func() { resty.SetCertificates(tls.Certificate{}) }()
+
+ // with client certs but without creds, should succeed
+ resp, err = resty.R().Get(secureBaseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // with client certs and creds, should get expected status code
+ resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+
+ // with client certs, creds shouldn't matter
+ resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ })
+}
+
+func TestTLSMutualAndBasicAuth(t *testing.T) {
+ Convey("Make a new controller", t, func() {
+ caCert, err := os.ReadFile(CACert)
+ So(err, ShouldBeNil)
+ caCertPool := x509.NewCertPool()
+ caCertPool.AppendCertsFromPEM(caCert)
+ htpasswdPath := test.MakeHtpasswdFile()
+ defer os.Remove(htpasswdPath)
+
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ secureBaseURL := test.GetSecureBaseURL(port)
+
+ resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
+ defer func() { resty.SetTLSClientConfig(nil) }()
+ conf := config.New()
+ conf.HTTP.Port = port
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ }
+ conf.HTTP.TLS = &config.TLSConfig{
+ Cert: ServerCert,
+ Key: ServerKey,
+ CACert: CACert,
+ }
+
+ ctlr := makeController(conf, t.TempDir())
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ // accessing insecure HTTP site should fail
+ resp, err := resty.R().Get(baseURL)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+
+ // without client certs and creds, should fail
+ _, err = resty.R().Get(secureBaseURL)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+
+ // with creds but without certs, should succeed
+ _, err = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+
+ // setup TLS mutual auth
+ cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key")
+ So(err, ShouldBeNil)
+
+ resty.SetCertificates(cert)
+ defer func() { resty.SetCertificates(tls.Certificate{}) }()
+
+ // with client certs but without creds, should get access error
+ resp, err = resty.R().Get(secureBaseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ // with client certs and creds, should get expected status code
+ resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+
+ resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ })
+}
+
+func TestTLSMutualAndBasicAuthAllowReadAccess(t *testing.T) {
+ Convey("Make a new controller", t, func() {
+ caCert, err := os.ReadFile(CACert)
+ So(err, ShouldBeNil)
+ caCertPool := x509.NewCertPool()
+ caCertPool.AppendCertsFromPEM(caCert)
+ htpasswdPath := test.MakeHtpasswdFile()
+ defer os.Remove(htpasswdPath)
+
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ secureBaseURL := test.GetSecureBaseURL(port)
+
+ resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
+ defer func() { resty.SetTLSClientConfig(nil) }()
+ conf := config.New()
+ conf.HTTP.Port = port
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ }
+ conf.HTTP.TLS = &config.TLSConfig{
+ Cert: ServerCert,
+ Key: ServerKey,
+ CACert: CACert,
+ }
+
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Repositories: config.Repositories{
+ AuthorizationAllRepos: config.PolicyGroup{
+ AnonymousPolicy: []string{"read"},
+ },
+ },
+ }
+
+ ctlr := makeController(conf, t.TempDir())
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ // accessing insecure HTTP site should fail
+ resp, err := resty.R().Get(baseURL)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+
+ // without client certs and creds, should fail
+ _, err = resty.R().Get(secureBaseURL)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+
+ // with creds but without certs, should succeed
+ _, err = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+
+ // setup TLS mutual auth
+ cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key")
+ So(err, ShouldBeNil)
+
+ resty.SetCertificates(cert)
+ defer func() { resty.SetCertificates(tls.Certificate{}) }()
+
+ // with client certs but without creds, reads should succeed
+ resp, err = resty.R().Get(secureBaseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // 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.StatusUnauthorized)
+
+ // with client certs and creds, should get expected status code
+ resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+
+ resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ })
+}
+
+func TestBearerAuthWithAllowReadAccess(t *testing.T) {
+ Convey("Make a new controller", t, func() {
+ authTestServer := authutils.MakeAuthTestServer(ServerKey, UnauthorizedNamespace)
+ defer authTestServer.Close()
+
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+
+ conf := config.New()
+ conf.HTTP.Port = port
+
+ aurl, err := url.Parse(authTestServer.URL)
+ So(err, ShouldBeNil)
+
+ conf.HTTP.Auth = &config.AuthConfig{
+ Bearer: &config.BearerConfig{
+ Cert: ServerCert,
+ Realm: authTestServer.URL + "/auth/token",
+ Service: aurl.Host,
+ },
+ }
+ ctlr := makeController(conf, t.TempDir())
+
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Repositories: config.Repositories{
+ AuthorizationAllRepos: config.PolicyGroup{
+ AnonymousPolicy: []string{"read"},
+ },
+ },
+ }
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ blob := []byte("hello, blob!")
+ digest := godigest.FromBytes(blob).String()
+
+ resp, err := resty.R().Get(baseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ authorizationHeader := authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
+ resp, err = resty.R().
+ SetQueryParam("service", authorizationHeader.Service).
+ SetQueryParam("scope", authorizationHeader.Scope).
+ Get(authorizationHeader.Realm)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ var goodToken authutils.AccessTokenResponse
+ err = json.Unmarshal(resp.Body(), &goodToken)
+ So(err, ShouldBeNil)
+
+ resp, err = resty.R().
+ SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
+ Get(baseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ resp, err = resty.R().Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ authorizationHeader = authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
+ resp, err = resty.R().
+ SetQueryParam("service", authorizationHeader.Service).
+ SetQueryParam("scope", authorizationHeader.Scope).
+ Get(authorizationHeader.Realm)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ err = json.Unmarshal(resp.Body(), &goodToken)
+ So(err, ShouldBeNil)
+
+ resp, err = resty.R().
+ SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
+ Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
+ loc := resp.Header().Get("Location")
+
+ resp, err = resty.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.StatusUnauthorized)
+
+ authorizationHeader = authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
+ resp, err = resty.R().
+ SetQueryParam("service", authorizationHeader.Service).
+ SetQueryParam("scope", authorizationHeader.Scope).
+ Get(authorizationHeader.Realm)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ err = json.Unmarshal(resp.Body(), &goodToken)
+ So(err, ShouldBeNil)
+
+ resp, err = resty.R().
+ SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))).
+ SetHeader("Content-Type", "application/octet-stream").
+ SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
+ SetQueryParam("digest", digest).
+ SetBody(blob).
+ Put(baseURL + loc)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ resp, err = resty.R().
+ SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
+ Get(baseURL + "/v2/" + AuthorizedNamespace + "/tags/list")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ authorizationHeader = authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
+ resp, err = resty.R().
+ SetQueryParam("service", authorizationHeader.Service).
+ SetQueryParam("scope", authorizationHeader.Scope).
+ Get(authorizationHeader.Realm)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ err = json.Unmarshal(resp.Body(), &goodToken)
+ So(err, ShouldBeNil)
+
+ resp, err = resty.R().
+ SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
+ Get(baseURL + "/v2/" + AuthorizedNamespace + "/tags/list")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ resp, err = resty.R().
+ Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ authorizationHeader = authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
+ resp, err = resty.R().
+ SetQueryParam("service", authorizationHeader.Service).
+ SetQueryParam("scope", authorizationHeader.Scope).
+ Get(authorizationHeader.Realm)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ var badToken authutils.AccessTokenResponse
+ err = json.Unmarshal(resp.Body(), &badToken)
+ So(err, ShouldBeNil)
+
+ resp, err = resty.R().
+ SetHeader("Authorization", fmt.Sprintf("Bearer %s", badToken.AccessToken)).
+ Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ })
+}
+
+func TestOpenIDMiddleware(t *testing.T) {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ defaultVal := true
+
+ conf := config.New()
+ conf.HTTP.Port = port
+
+ testCases := []struct {
+ testCaseName string
+ address string
+ externalURL string
+ }{
+ {
+ address: "0.0.0.0",
+ externalURL: fmt.Sprintf("http://%s", net.JoinHostPort(conf.HTTP.Address, conf.HTTP.Port)),
+ testCaseName: "with ExternalURL provided in config",
+ },
+ {
+ address: "127.0.0.1",
+ externalURL: "",
+ testCaseName: "without ExternalURL provided in config",
+ },
+ }
+
+ // need a username different than ldap one, to test both logic
+ content := fmt.Sprintf("%s:$2y$05$hlbSXDp6hzDLu6VwACS39ORvVRpr3OMR4RlJ31jtlaOEGnPjKZI1m\n", htpasswdUsername)
+ htpasswdPath := test.MakeHtpasswdFileFromString(content)
+
+ defer os.Remove(htpasswdPath)
+
+ ldapServer := newTestLDAPServer()
+ port = test.GetFreePort()
+
+ ldapPort, err := strconv.Atoi(port)
+ if err != nil {
+ panic(err)
+ }
+
+ ldapServer.Start(ldapPort)
+ defer ldapServer.Stop()
+
+ mockOIDCServer, err := authutils.MockOIDCRun()
+ if err != nil {
+ panic(err)
+ }
+
+ defer func() {
+ err := mockOIDCServer.Shutdown()
+ if err != nil {
+ panic(err)
+ }
+ }()
+
+ mockOIDCConfig := mockOIDCServer.Config()
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ LDAP: &config.LDAPConfig{
+ Insecure: true,
+ Address: LDAPAddress,
+ Port: ldapPort,
+ BindDN: LDAPBindDN,
+ BindPassword: LDAPBindPassword,
+ BaseDN: LDAPBaseDN,
+ UserAttribute: "uid",
+ },
+ OpenID: &config.OpenIDConfig{
+ Providers: map[string]config.OpenIDProviderConfig{
+ "oidc": {
+ ClientID: mockOIDCConfig.ClientID,
+ ClientSecret: mockOIDCConfig.ClientSecret,
+ KeyPath: "",
+ Issuer: mockOIDCConfig.Issuer,
+ Scopes: []string{"openid", "email"},
+ },
+ // just for the constructor coverage
+ "github": {
+ ClientID: mockOIDCConfig.ClientID,
+ ClientSecret: mockOIDCConfig.ClientSecret,
+ KeyPath: "",
+ Issuer: mockOIDCConfig.Issuer,
+ Scopes: []string{"openid", "email"},
+ },
+ },
+ },
+ }
+
+ searchConfig := &extconf.SearchConfig{
+ BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
+ }
+
+ // UI is enabled because we also want to test access on the mgmt route
+ uiConfig := &extconf.UIConfig{
+ BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
+ }
+
+ conf.Extensions = &extconf.ExtensionConfig{
+ Search: searchConfig,
+ UI: uiConfig,
+ }
+
+ ctlr := api.NewController(conf)
+
+ for _, testcase := range testCases {
+ t.Run(testcase.testCaseName, func(t *testing.T) {
+ Convey("make controller", t, func() {
+ dir := t.TempDir()
+
+ ctlr.Config.Storage.RootDirectory = dir
+ ctlr.Config.HTTP.ExternalURL = testcase.externalURL
+ ctlr.Config.HTTP.Address = testcase.address
+ cm := test.NewControllerManager(ctlr)
+
+ cm.StartServer()
+ defer cm.StopServer()
+ test.WaitTillServerReady(baseURL)
+
+ Convey("browser client requests", func() {
+ Convey("login with no provider supplied", func() {
+ client := resty.New()
+ client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+ // first login user
+ resp, err := client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ SetQueryParam("provider", "unknown").
+ Get(baseURL + constants.LoginPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+ })
+
+ //nolint: dupl
+ Convey("make sure sessions are not used without UI header value", func() {
+ sessionsNo, err := getNumberOfSessions(conf.Storage.RootDirectory)
+ So(err, ShouldBeNil)
+ So(sessionsNo, ShouldEqual, 0)
+
+ client := resty.New()
+
+ // without header should not create session
+ resp, err := client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ sessionsNo, err = getNumberOfSessions(conf.Storage.RootDirectory)
+ So(err, ShouldBeNil)
+ So(sessionsNo, ShouldEqual, 0)
+
+ client.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
+
+ resp, err = client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ sessionsNo, err = getNumberOfSessions(conf.Storage.RootDirectory)
+ So(err, ShouldBeNil)
+ So(sessionsNo, ShouldEqual, 1)
+
+ // set cookies
+ client.SetCookies(resp.Cookies())
+
+ // should get same cookie
+ resp, err = client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ sessionsNo, err = getNumberOfSessions(conf.Storage.RootDirectory)
+ So(err, ShouldBeNil)
+ So(sessionsNo, ShouldEqual, 1)
+
+ resp, err = client.R().
+ SetBasicAuth(htpasswdUsername, passphrase).
+ Get(baseURL + constants.FullMgmt)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ client.SetCookies(resp.Cookies())
+
+ // call endpoint with session, without credentials, (added to client after previous request)
+ resp, err = client.R().
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ resp, err = client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ sessionsNo, err = getNumberOfSessions(conf.Storage.RootDirectory)
+ So(err, ShouldBeNil)
+ So(sessionsNo, ShouldEqual, 1)
+ })
+
+ Convey("login with openid and get catalog with session", func() {
+ client := resty.New()
+ client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+ client.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
+
+ Convey("with callback_ui value provided", func() {
+ // first login user
+ resp, err := client.R().
+ SetQueryParam("provider", "oidc").
+ SetQueryParam("callback_ui", baseURL+"/v2/").
+ Get(baseURL + constants.LoginPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ })
+
+ // first login user
+ resp, err := client.R().
+ SetQueryParam("provider", "oidc").
+ Get(baseURL + constants.LoginPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ client.SetCookies(resp.Cookies())
+
+ // call endpoint with session (added to client after previous request)
+ resp, err = client.R().
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // logout with options method for coverage
+ resp, err = client.R().
+ Options(baseURL + constants.LogoutPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+
+ // logout user
+ resp, err = client.R().
+ Post(baseURL + constants.LogoutPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // calling endpoint should fail with unauthorized access
+ resp, err = client.R().
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ })
+
+ //nolint: dupl
+ Convey("login with basic auth(htpasswd) and get catalog with session", func() {
+ client := resty.New()
+ client.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
+
+ // without creds, should get access error
+ resp, err := client.R().Get(baseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ var e apiErr.Error
+ err = json.Unmarshal(resp.Body(), &e)
+ So(err, ShouldBeNil)
+
+ // first login user
+ // with creds, should get expected status code
+ resp, err = client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ resp, err = client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ resp, err = client.R().
+ SetBasicAuth(htpasswdUsername, passphrase).
+ Get(baseURL + constants.FullMgmt)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ client.SetCookies(resp.Cookies())
+
+ // call endpoint with session, without credentials, (added to client after previous request)
+ resp, err = client.R().
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ resp, err = client.R().
+ Get(baseURL + constants.FullMgmt)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // logout user
+ resp, err = client.R().
+ Post(baseURL + constants.LogoutPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // calling endpoint should fail with unauthorized access
+ resp, err = client.R().
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ })
+
+ //nolint: dupl
+ Convey("login with ldap and get catalog", func() {
+ client := resty.New()
+ client.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
+
+ // without creds, should get access error
+ resp, err := client.R().Get(baseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ var e apiErr.Error
+ err = json.Unmarshal(resp.Body(), &e)
+ So(err, ShouldBeNil)
+
+ // first login user
+ // with creds, should get expected status code
+ resp, err = client.R().SetBasicAuth(username, passphrase).Get(baseURL)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ resp, err = client.R().SetBasicAuth(username, passphrase).Get(baseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ resp, err = client.R().
+ SetBasicAuth(username, passphrase).
+ Get(baseURL + constants.FullMgmt)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ client.SetCookies(resp.Cookies())
+
+ // call endpoint with session, without credentials, (added to client after previous request)
+ resp, err = client.R().
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ resp, err = client.R().
+ Get(baseURL + constants.FullMgmt)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // logout user
+ resp, err = client.R().
+ Post(baseURL + constants.LogoutPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // calling endpoint should fail with unauthorized access
+ resp, err = client.R().
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ })
+
+ Convey("unauthenticated catalog request", func() {
+ client := resty.New()
+
+ // mgmt should work both unauthenticated and authenticated
+ resp, err := client.R().
+ Get(baseURL + constants.FullMgmt)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // call endpoint without session
+ resp, err = client.R().
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ })
+ })
+ })
+ })
+ }
+}
+
+func TestIsOpenIDEnabled(t *testing.T) {
+ Convey("make oidc server", t, func() {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+
+ conf := config.New()
+ conf.HTTP.Port = port
+
+ mockOIDCServer, err := authutils.MockOIDCRun()
+ if err != nil {
+ panic(err)
+ }
+
+ defer func() {
+ err := mockOIDCServer.Shutdown()
+ if err != nil {
+ panic(err)
+ }
+ }()
+
+ 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"},
+ },
+ },
+ },
+ }
+
+ ctlr := api.NewController(conf)
+
+ ctlr.Config.Storage.RootDirectory = rootDir
+
+ cm := test.NewControllerManager(ctlr)
+
+ 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)
+ })
+
+ Convey("Unsupported provider", func() {
+ mockOIDCConfig := mockOIDCServer.Config()
+ conf.HTTP.Auth = &config.AuthConfig{
+ OpenID: &config.OpenIDConfig{
+ Providers: map[string]config.OpenIDProviderConfig{
+ "invalidProvider": {
+ ClientID: mockOIDCConfig.ClientID,
+ ClientSecret: mockOIDCConfig.ClientSecret,
+ KeyPath: "",
+ Issuer: mockOIDCConfig.Issuer,
+ Scopes: []string{"email", "groups"},
+ },
+ },
+ },
+ }
+
+ ctlr := api.NewController(conf)
+
+ ctlr.Config.Storage.RootDirectory = rootDir
+
+ cm := test.NewControllerManager(ctlr)
+
+ cm.StartServer()
+ 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.StatusOK)
+ })
+ })
+}
+
+func TestAuthnSessionErrors(t *testing.T) {
+ Convey("make controller", t, func() {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ defaultVal := true
+
+ conf := config.New()
+ conf.HTTP.Port = port
+ invalidSessionID := "sessionID"
+
+ // need a username different than ldap one, to test both logic
+ content := fmt.Sprintf("%s:$2y$05$hlbSXDp6hzDLu6VwACS39ORvVRpr3OMR4RlJ31jtlaOEGnPjKZI1m\n", htpasswdUsername)
+
+ htpasswdPath := test.MakeHtpasswdFileFromString(content)
+ defer os.Remove(htpasswdPath)
+
+ ldapServer := newTestLDAPServer()
+ port = test.GetFreePort()
+
+ ldapPort, err := strconv.Atoi(port)
+ if err != nil {
+ panic(err)
+ }
+
+ ldapServer.Start(ldapPort)
+ defer ldapServer.Stop()
+
+ mockOIDCServer, err := authutils.MockOIDCRun()
+ if err != nil {
+ panic(err)
+ }
+
+ defer func() {
+ err := mockOIDCServer.Shutdown()
+ if err != nil {
+ panic(err)
+ }
+ }()
+
+ rootDir := t.TempDir()
+
+ mockOIDCConfig := mockOIDCServer.Config()
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ LDAP: &config.LDAPConfig{
+ Insecure: true,
+ Address: LDAPAddress,
+ Port: ldapPort,
+ BindDN: LDAPBindDN,
+ BindPassword: LDAPBindPassword,
+ BaseDN: LDAPBaseDN,
+ UserAttribute: "uid",
+ },
+ OpenID: &config.OpenIDConfig{
+ Providers: map[string]config.OpenIDProviderConfig{
+ "oidc": {
+ ClientID: mockOIDCConfig.ClientID,
+ ClientSecret: mockOIDCConfig.ClientSecret,
+ KeyPath: "",
+ Issuer: mockOIDCConfig.Issuer,
+ Scopes: []string{"email", "groups"},
+ },
+ },
+ },
+ }
+
+ uiConfig := &extconf.UIConfig{
+ BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
+ }
+
+ searchConfig := &extconf.SearchConfig{
+ BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
+ }
+
+ conf.Extensions = &extconf.ExtensionConfig{
+ UI: uiConfig,
+ Search: searchConfig,
+ }
+
+ ctlr := api.NewController(conf)
+
+ ctlr.Config.Storage.RootDirectory = rootDir
+
+ cm := test.NewControllerManager(ctlr)
+
+ cm.StartServer()
+ defer cm.StopServer()
+ test.WaitTillServerReady(baseURL)
+
+ Convey("trigger basic authn middle(htpasswd) error", func() {
+ client := resty.New()
+
+ ctlr.MetaDB = mocks.MetaDBMock{
+ SetUserGroupsFn: func(ctx context.Context, groups []string) error {
+ return ErrUnexpectedError
+ },
+ }
+
+ resp, err := client.R().
+ SetBasicAuth(htpasswdUsername, passphrase).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
+ })
+
+ Convey("trigger basic authn middle(ldap) error", func() {
+ client := resty.New()
+
+ ctlr.MetaDB = mocks.MetaDBMock{
+ SetUserGroupsFn: func(ctx context.Context, groups []string) error {
+ return ErrUnexpectedError
+ },
+ }
+
+ resp, err := client.R().
+ SetBasicAuth(username, passphrase).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
+ })
+
+ Convey("trigger updateUserData error", func() {
+ client := resty.New()
+ client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+
+ ctlr.MetaDB = mocks.MetaDBMock{
+ SetUserGroupsFn: func(ctx context.Context, groups []string) error {
+ return ErrUnexpectedError
+ },
+ }
+
+ resp, err := client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ SetQueryParam("provider", "oidc").
+ Get(baseURL + constants.LoginPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
+ })
+
+ Convey("trigger session middle metaDB errors", func() {
+ client := resty.New()
+ client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+
+ user := mockoidc.DefaultUser()
+ user.Groups = []string{"group1", "group2"}
+
+ mockOIDCServer.QueueUser(user)
+
+ ctlr.MetaDB = mocks.MetaDBMock{}
+
+ // first login user
+ resp, err := client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ SetQueryParam("provider", "oidc").
+ Get(baseURL + constants.LoginPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ Convey("trigger session middle error internal server error", func() {
+ cookies := resp.Cookies()
+
+ client.SetCookies(cookies)
+
+ ctlr.MetaDB = mocks.MetaDBMock{
+ GetUserGroupsFn: func(ctx context.Context) ([]string, error) {
+ return []string{}, ErrUnexpectedError
+ },
+ }
+
+ // call endpoint with session (added to client after previous request)
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
+ })
+
+ Convey("trigger session middle error GetUserGroups not found", func() {
+ cookies := resp.Cookies()
+
+ client.SetCookies(cookies)
+
+ ctlr.MetaDB = mocks.MetaDBMock{
+ GetUserGroupsFn: func(ctx context.Context) ([]string, error) {
+ return []string{}, errors.ErrUserDataNotFound
+ },
+ }
+
+ // call endpoint with session (added to client after previous request)
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ })
+ })
+
+ Convey("trigger no email error in routes(callback)", func() {
+ user := mockoidc.DefaultUser()
+ user.Email = ""
+
+ mockOIDCServer.QueueUser(user)
+
+ client := resty.New()
+ client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+
+ client.SetCookie(&http.Cookie{Name: "session"})
+
+ // call endpoint with session (added to client after previous request)
+ resp, err := client.R().
+ SetQueryParam("provider", "oidc").
+ Get(baseURL + constants.LoginPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ })
+
+ Convey("trigger session save error in routes(callback)", func() {
+ err := os.Chmod(rootDir, 0o000)
+ So(err, ShouldBeNil)
+
+ defer func() {
+ err := os.Chmod(rootDir, storageConstants.DefaultDirPerms)
+ So(err, ShouldBeNil)
+ }()
+
+ client := resty.New()
+ client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+
+ // first login user
+ resp, err := client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ SetQueryParam("provider", "oidc").
+ Get(baseURL + constants.LoginPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
+ })
+
+ Convey("trigger session save error in basicAuthn", func() {
+ err := os.Chmod(rootDir, 0o000)
+ So(err, ShouldBeNil)
+
+ defer func() {
+ err := os.Chmod(rootDir, storageConstants.DefaultDirPerms)
+ So(err, ShouldBeNil)
+ }()
+
+ client := resty.New()
+ client.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
+
+ // first htpasswd saveSessionLoggedUser() error
+ resp, err := client.R().
+ SetBasicAuth(htpasswdUsername, passphrase).
+ Get(baseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
+
+ // second ldap saveSessionLoggedUser() error
+ resp, err = client.R().
+ SetBasicAuth(username, passphrase).
+ Get(baseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
+ })
+
+ Convey("trigger session middle errors", func() {
+ client := resty.New()
+ client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+
+ user := mockoidc.DefaultUser()
+ user.Groups = []string{"group1", "group2"}
+
+ mockOIDCServer.QueueUser(user)
+
+ // first login user
+ resp, err := client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ SetQueryParam("provider", "oidc").
+ Get(baseURL + constants.LoginPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ Convey("trigger bad session encoding error in authn", func() {
+ cookies := resp.Cookies()
+ for _, cookie := range cookies {
+ if cookie.Name == "session" {
+ cookie.Value = "badSessionValue"
+ }
+ }
+
+ client.SetCookies(cookies)
+
+ // call endpoint with session (added to client after previous request)
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ })
+
+ Convey("web request without cookies", func() {
+ client.SetCookie(&http.Cookie{})
+
+ // call endpoint with session (added to client after previous request)
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ })
+
+ Convey("web request with userless cookie", func() {
+ // first get session
+ session, err := ctlr.CookieStore.Get(resp.RawResponse.Request, "session")
+ So(err, ShouldBeNil)
+
+ session.ID = invalidSessionID
+ session.IsNew = false
+ session.Values["authStatus"] = true
+
+ cookieStore, ok := ctlr.CookieStore.(*sessions.FilesystemStore)
+ So(ok, ShouldBeTrue)
+
+ // first encode sessionID
+ encoded, err := securecookie.EncodeMulti(session.Name(), session.ID,
+ cookieStore.Codecs...)
+ So(err, ShouldBeNil)
+
+ // save cookie
+ cookie := sessions.NewCookie(session.Name(), encoded, session.Options)
+ client.SetCookie(cookie)
+
+ // encode session values and save on disk
+ encoded, err = securecookie.EncodeMulti(session.Name(), session.Values,
+ cookieStore.Codecs...)
+ So(err, ShouldBeNil)
+
+ filename := filepath.Join(rootDir, "_sessions", "session_"+session.ID)
+
+ err = os.WriteFile(filename, []byte(encoded), 0o600)
+ So(err, ShouldBeNil)
+
+ // call endpoint with session (added to client after previous request)
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ })
+
+ Convey("web request with authStatus false cookie", func() {
+ // first get session
+ session, err := ctlr.CookieStore.Get(resp.RawResponse.Request, "session")
+ So(err, ShouldBeNil)
+
+ session.ID = invalidSessionID
+ session.IsNew = false
+ session.Values["authStatus"] = false
+ session.Values["username"] = username
+
+ cookieStore, ok := ctlr.CookieStore.(*sessions.FilesystemStore)
+ So(ok, ShouldBeTrue)
+
+ // first encode sessionID
+ encoded, err := securecookie.EncodeMulti(session.Name(), session.ID,
+ cookieStore.Codecs...)
+ So(err, ShouldBeNil)
+
+ // save cookie
+ cookie := sessions.NewCookie(session.Name(), encoded, session.Options)
+ client.SetCookie(cookie)
+
+ // encode session values and save on disk
+ encoded, err = securecookie.EncodeMulti(session.Name(), session.Values,
+ cookieStore.Codecs...)
+ So(err, ShouldBeNil)
+
+ filename := filepath.Join(rootDir, "_sessions", "session_"+session.ID)
+
+ err = os.WriteFile(filename, []byte(encoded), 0o600)
+ So(err, ShouldBeNil)
+
+ // call endpoint with session (added to client after previous request)
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ })
+ })
+ })
+}
+
+func TestAuthnMetaDBErrors(t *testing.T) {
+ Convey("make controller", t, func() {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ conf := config.New()
+ conf.HTTP.Port = port
+
+ htpasswdPath := test.MakeHtpasswdFile()
+ defer os.Remove(htpasswdPath)
+
+ mockOIDCServer, err := authutils.MockOIDCRun()
+ if err != nil {
+ panic(err)
+ }
+
+ defer func() {
+ err := mockOIDCServer.Shutdown()
+ if err != nil {
+ panic(err)
+ }
+ }()
+
+ rootDir := t.TempDir()
+
+ mockOIDCConfig := mockOIDCServer.Config()
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ OpenID: &config.OpenIDConfig{
+ Providers: map[string]config.OpenIDProviderConfig{
+ "oidc": {
+ ClientID: mockOIDCConfig.ClientID,
+ ClientSecret: mockOIDCConfig.ClientSecret,
+ KeyPath: "",
+ Issuer: mockOIDCConfig.Issuer,
+ Scopes: []string{"openid", "email"},
+ },
+ },
+ },
+ }
+
+ ctlr := api.NewController(conf)
+
+ ctlr.Config.Storage.RootDirectory = rootDir
+
+ cm := test.NewControllerManager(ctlr)
+
+ cm.StartServer()
+ defer cm.StopServer()
+ test.WaitTillServerReady(baseURL)
+
+ Convey("trigger basic authn middle(htpasswd) error", func() {
+ client := resty.New()
+
+ ctlr.MetaDB = mocks.MetaDBMock{
+ SetUserGroupsFn: func(ctx context.Context, groups []string) error {
+ return ErrUnexpectedError
+ },
+ }
+
+ resp, err := client.R().
+ SetBasicAuth(username, passphrase).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
+ })
+
+ Convey("trigger session middle metaDB errors", func() {
+ client := resty.New()
+ client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+
+ user := mockoidc.DefaultUser()
+ user.Groups = []string{"group1", "group2"}
+
+ mockOIDCServer.QueueUser(user)
+
+ // first login user
+ resp, err := client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ SetQueryParam("provider", "oidc").
+ Get(baseURL + constants.LoginPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ Convey("trigger session middle error", func() {
+ cookies := resp.Cookies()
+
+ client.SetCookies(cookies)
+
+ ctlr.MetaDB = mocks.MetaDBMock{
+ GetUserGroupsFn: func(ctx context.Context) ([]string, error) {
+ return []string{}, ErrUnexpectedError
+ },
+ }
+
+ // call endpoint with session (added to client after previous request)
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
+ })
+ })
+ })
+}
+
+func TestGetUsername(t *testing.T) {
+ Convey("Make a new controller", t, func() {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+
+ htpasswdPath := test.MakeHtpasswdFileFromString(getCredString(username, passphrase))
+ defer os.Remove(htpasswdPath)
+
+ conf := config.New()
+ conf.HTTP.Port = port
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ }
+
+ dir := t.TempDir()
+ ctlr := makeController(conf, dir)
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ resp, err := resty.R().Get(baseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ // test base64 encode
+ resp, err = resty.R().SetHeader("Authorization", "Basic should fail").Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ // test "username:password" encoding
+ resp, err = resty.R().SetHeader("Authorization", "Basic dGVzdA==").Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ // failed parsing authorization header
+ resp, err = resty.R().SetHeader("Authorization", "Basic ").Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ 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)
+ })
+}
+
+func TestAllowedMethodsHeaderAPIKey(t *testing.T) {
+ defaultVal := true
+
+ Convey("Test http options response", t, func() {
+ conf := config.New()
+ port := test.GetFreePort()
+ conf.HTTP.Port = port
+ conf.HTTP.Auth.APIKey = defaultVal
+ baseURL := test.GetBaseURL(port)
+
+ ctlr := api.NewController(conf)
+ ctlr.Config.Storage.RootDirectory = t.TempDir()
+
+ ctrlManager := test.NewControllerManager(ctlr)
+
+ ctrlManager.StartAndWait(port)
+ defer ctrlManager.StopServer()
+
+ resp, _ := resty.R().Options(baseURL + constants.APIKeyPath)
+ So(resp, ShouldNotBeNil)
+ So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,POST,DELETE,OPTIONS")
+ So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
+ })
+}
+
+func TestAPIKeys(t *testing.T) {
+ Convey("Make a new controller", t, func() {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+
+ conf := config.New()
+ conf.HTTP.Port = port
+
+ htpasswdPath := test.MakeHtpasswdFile()
+ defer os.Remove(htpasswdPath)
+
+ mockOIDCServer, err := authutils.MockOIDCRun()
+ if err != nil {
+ panic(err)
+ }
+
+ defer func() {
+ err := mockOIDCServer.Shutdown()
+ if err != nil {
+ panic(err)
+ }
+ }()
+
+ mockOIDCConfig := mockOIDCServer.Config()
+ defaultVal := true
+
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ OpenID: &config.OpenIDConfig{
+ Providers: map[string]config.OpenIDProviderConfig{
+ "oidc": {
+ ClientID: mockOIDCConfig.ClientID,
+ ClientSecret: mockOIDCConfig.ClientSecret,
+ KeyPath: "",
+ Issuer: mockOIDCConfig.Issuer,
+ Scopes: []string{"openid", "email", "groups"},
+ },
+ },
+ },
+ APIKey: defaultVal,
+ }
+
+ conf.HTTP.AccessControl = &config.AccessControlConfig{}
+
+ conf.Extensions = &extconf.ExtensionConfig{}
+ conf.Extensions.Search = &extconf.SearchConfig{}
+ conf.Extensions.Search.Enable = &defaultVal
+ conf.Extensions.Search.CVE = nil
+ conf.Extensions.UI = &extconf.UIConfig{}
+ conf.Extensions.UI.Enable = &defaultVal
+
+ ctlr := api.NewController(conf)
+ dir := t.TempDir()
+
+ ctlr.Config.Storage.RootDirectory = dir
+
+ cm := test.NewControllerManager(ctlr)
+
+ cm.StartServer()
+ defer cm.StopServer()
+ test.WaitTillServerReady(baseURL)
+
+ payload := api.APIKeyPayload{
+ Label: "test",
+ Scopes: []string{"test"},
+ }
+ reqBody, err := json.Marshal(payload)
+ So(err, ShouldBeNil)
+
+ Convey("API key retrieved with basic auth", func() {
+ resp, err := resty.R().
+ SetBody(reqBody).
+ SetBasicAuth("test", "test").
+ Post(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ user := mockoidc.DefaultUser()
+
+ // get API key and email from apikey route response
+ var apiKeyResponse apiKeyResponse
+ err = json.Unmarshal(resp.Body(), &apiKeyResponse)
+ So(err, ShouldBeNil)
+
+ email := user.Email
+ So(email, ShouldNotBeEmpty)
+
+ resp, err = resty.R().
+ SetBasicAuth("test", apiKeyResponse.APIKey).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // get API key list with basic auth
+ resp, err = resty.R().
+ SetBasicAuth("test", "test").
+ Get(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ var apiKeyListResponse apiKeyListResponse
+ err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
+ So(err, ShouldBeNil)
+ So(len(apiKeyListResponse.APIKeys), ShouldEqual, 1)
+ So(apiKeyListResponse.APIKeys[0].CreatedAt, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatedAt)
+ So(apiKeyListResponse.APIKeys[0].CreatorUA, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatorUA)
+ So(apiKeyListResponse.APIKeys[0].Label, ShouldEqual, apiKeyResponse.APIKeyDetails.Label)
+ So(apiKeyListResponse.APIKeys[0].Scopes, ShouldEqual, apiKeyResponse.APIKeyDetails.Scopes)
+ So(apiKeyListResponse.APIKeys[0].UUID, ShouldEqual, apiKeyResponse.APIKeyDetails.UUID)
+
+ // add another one
+ resp, err = resty.R().
+ SetBody(reqBody).
+ SetBasicAuth("test", "test").
+ Post(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ err = json.Unmarshal(resp.Body(), &apiKeyResponse)
+ So(err, ShouldBeNil)
+
+ resp, err = resty.R().
+ SetBasicAuth("test", apiKeyResponse.APIKey).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // get API key list with api key auth
+ resp, err = resty.R().
+ SetBasicAuth("test", apiKeyResponse.APIKey).
+ Get(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
+ So(err, ShouldBeNil)
+ So(len(apiKeyListResponse.APIKeys), ShouldEqual, 2)
+ })
+
+ Convey("API key retrieved with openID and with no expire", func() {
+ client := resty.New()
+ client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+
+ // first login user
+ resp, err := client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ SetQueryParam("provider", "oidc").
+ Get(baseURL + constants.LoginPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+
+ cookies := resp.Cookies()
+
+ // call endpoint without session
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Post(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ client.SetCookies(cookies)
+
+ // call endpoint with session ( added to client after previous request)
+ resp, err = client.R().
+ SetBody(reqBody).
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Post(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ user := mockoidc.DefaultUser()
+
+ // get API key and email from apikey route response
+ var apiKeyResponse apiKeyResponse
+ err = json.Unmarshal(resp.Body(), &apiKeyResponse)
+ So(err, ShouldBeNil)
+
+ email := user.Email
+ So(email, ShouldNotBeEmpty)
+
+ // get API key list
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Get(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ var apiKeyListResponse apiKeyListResponse
+
+ err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
+ So(err, ShouldBeNil)
+ So(len(apiKeyListResponse.APIKeys), ShouldEqual, 1)
+ So(apiKeyListResponse.APIKeys[0].CreatedAt, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatedAt)
+ So(apiKeyListResponse.APIKeys[0].CreatorUA, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatorUA)
+ So(apiKeyListResponse.APIKeys[0].Label, ShouldEqual, apiKeyResponse.APIKeyDetails.Label)
+ So(apiKeyListResponse.APIKeys[0].Scopes, ShouldEqual, apiKeyResponse.APIKeyDetails.Scopes)
+ So(apiKeyListResponse.APIKeys[0].UUID, ShouldEqual, apiKeyResponse.APIKeyDetails.UUID)
+
+ resp, err = client.R().
+ SetBasicAuth(email, apiKeyResponse.APIKey).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // trigger errors
+ ctlr.MetaDB = mocks.MetaDBMock{
+ GetUserAPIKeyInfoFn: func(hashedKey string) (string, error) {
+ return "", ErrUnexpectedError
+ },
+ }
+
+ resp, err = client.R().
+ SetBasicAuth(email, apiKeyResponse.APIKey).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
+
+ ctlr.MetaDB = mocks.MetaDBMock{
+ GetUserAPIKeyInfoFn: func(hashedKey string) (string, error) {
+ return user.Email, nil
+ },
+ GetUserGroupsFn: func(ctx context.Context) ([]string, error) {
+ return []string{}, ErrUnexpectedError
+ },
+ }
+
+ resp, err = client.R().
+ SetBasicAuth(email, apiKeyResponse.APIKey).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
+
+ ctlr.MetaDB = mocks.MetaDBMock{
+ GetUserAPIKeyInfoFn: func(hashedKey string) (string, error) {
+ return user.Email, nil
+ },
+ UpdateUserAPIKeyLastUsedFn: func(ctx context.Context, hashedKey string) error {
+ return ErrUnexpectedError
+ },
+ }
+
+ resp, err = client.R().
+ SetBasicAuth(email, apiKeyResponse.APIKey).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
+
+ client = resty.New()
+
+ // call endpoint without session
+ resp, err = client.R().
+ SetBody(reqBody).
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Post(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ })
+
+ Convey("API key retrieved with openID and with long expire", func() {
+ payload := api.APIKeyPayload{
+ Label: "test",
+ Scopes: []string{"test"},
+ ExpirationDate: time.Now().Add(time.Hour).Local().Format(constants.APIKeyTimeFormat),
+ }
+
+ reqBody, err := json.Marshal(payload)
+ So(err, ShouldBeNil)
+
+ client := resty.New()
+
+ // mgmt should work both unauthenticated and authenticated
+ resp, err := client.R().Get(baseURL + constants.FullMgmt)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+ // first login user
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ SetQueryParam("provider", "oidc").
+ Get(baseURL + constants.LoginPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ client.SetCookies(resp.Cookies())
+
+ // call endpoint with session ( added to client after previous request)
+ resp, err = client.R().
+ SetBody(reqBody).
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Post(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ var apiKeyResponse apiKeyResponse
+ err = json.Unmarshal(resp.Body(), &apiKeyResponse)
+ So(err, ShouldBeNil)
+
+ // get API key list
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Get(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ var apiKeyListResponse apiKeyListResponse
+
+ err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
+ So(err, ShouldBeNil)
+ So(len(apiKeyListResponse.APIKeys), ShouldEqual, 1)
+ So(apiKeyListResponse.APIKeys[0].CreatedAt, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatedAt)
+ So(apiKeyListResponse.APIKeys[0].CreatorUA, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatorUA)
+ So(apiKeyListResponse.APIKeys[0].Label, ShouldEqual, apiKeyResponse.APIKeyDetails.Label)
+ So(apiKeyListResponse.APIKeys[0].Scopes, ShouldEqual, apiKeyResponse.APIKeyDetails.Scopes)
+ So(apiKeyListResponse.APIKeys[0].UUID, ShouldEqual, apiKeyResponse.APIKeyDetails.UUID)
+
+ user := mockoidc.DefaultUser()
+ email := user.Email
+ So(email, ShouldNotBeEmpty)
+
+ resp, err = client.R().
+ SetBasicAuth(email, apiKeyResponse.APIKey).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // auth with API key
+ // we need new client without session cookie set
+ client = resty.New()
+ client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+
+ resp, err = client.R().
+ SetBasicAuth(email, apiKeyResponse.APIKey).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ resp, err = client.R().
+ SetBasicAuth(email, apiKeyResponse.APIKey).
+ Get(baseURL + constants.FullMgmt)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // get API key list
+ resp, err = resty.R().
+ SetBasicAuth(email, apiKeyResponse.APIKey).
+ Get(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
+ So(err, ShouldBeNil)
+ So(len(apiKeyListResponse.APIKeys), ShouldEqual, 1)
+
+ // invalid api keys
+ resp, err = client.R().
+ SetBasicAuth("invalidEmail", apiKeyResponse.APIKey).
+ Get(baseURL + constants.FullMgmt)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ resp, err = client.R().
+ SetBasicAuth(email, "noprefixAPIKey").
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ resp, err = client.R().
+ SetBasicAuth(email, "zak_notworkingAPIKey").
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ userAc := reqCtx.NewUserAccessControl()
+ userAc.SetUsername(email)
+ ctx := userAc.DeriveContext(context.Background())
+
+ err = ctlr.MetaDB.DeleteUserData(ctx)
+ So(err, ShouldBeNil)
+
+ resp, err = client.R().
+ SetBasicAuth(email, apiKeyResponse.APIKey).
+ Get(baseURL + constants.FullMgmt)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
+
+ client = resty.New()
+ client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+
+ // without creds should work
+ resp, err = client.R().Get(baseURL + constants.FullMgmt)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // login again
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ SetQueryParam("provider", "oidc").
+ Get(baseURL + constants.LoginPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ client.SetCookies(resp.Cookies())
+
+ resp, err = client.R().
+ SetBody(reqBody).
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Post(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ err = json.Unmarshal(resp.Body(), &apiKeyResponse)
+ So(err, ShouldBeNil)
+
+ // should work with session
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Get(baseURL + constants.FullMgmt)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // should work with api key
+ resp, err = client.R().
+ SetBasicAuth(email, apiKeyResponse.APIKey).
+ Get(baseURL + constants.FullMgmt)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ resp, err = client.R().
+ SetBasicAuth(email, apiKeyResponse.APIKey).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ err = json.Unmarshal(resp.Body(), &apiKeyResponse)
+ So(err, ShouldBeNil)
+
+ // delete api key
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ SetQueryParam("id", apiKeyResponse.UUID).
+ Delete(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // apiKey removed, should get 401
+ resp, err = client.R().
+ SetBasicAuth(email, apiKeyResponse.APIKey).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Delete(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+
+ resp, err = client.R().
+ SetBasicAuth(email, apiKeyResponse.APIKey).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // get API key list
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Get(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
+ So(err, ShouldBeNil)
+ So(len(apiKeyListResponse.APIKeys), ShouldEqual, 0)
+
+ resp, err = client.R().
+ SetBasicAuth("test", "test").
+ SetQueryParam("id", apiKeyResponse.UUID).
+ Delete(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // unsupported method
+ resp, err = client.R().
+ Put(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
+ })
+
+ Convey("API key retrieved with openID and with short expire", func() {
+ expirationDate := time.Now().Add(1 * time.Second).Local().Round(time.Second)
+ payload := api.APIKeyPayload{
+ Label: "test",
+ Scopes: []string{"test"},
+ ExpirationDate: expirationDate.Format(constants.APIKeyTimeFormat),
+ }
+
+ reqBody, err := json.Marshal(payload)
+ So(err, ShouldBeNil)
+
+ client := resty.New()
+
+ client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+ // first login user
+ resp, err := client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ SetQueryParam("provider", "oidc").
+ Get(baseURL + constants.LoginPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ client.SetCookies(resp.Cookies())
+
+ // call endpoint with session (added to client after previous request)
+ resp, err = client.R().
+ SetBody(reqBody).
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Post(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ var apiKeyResponse apiKeyResponse
+ err = json.Unmarshal(resp.Body(), &apiKeyResponse)
+ So(err, ShouldBeNil)
+
+ user := mockoidc.DefaultUser()
+ email := user.Email
+ So(email, ShouldNotBeEmpty)
+
+ // get API key list
+ resp, err = client.R().
+ SetBasicAuth(email, apiKeyResponse.APIKey).
+ Get(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ var apiKeyListResponse apiKeyListResponse
+
+ err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
+ So(err, ShouldBeNil)
+ So(len(apiKeyListResponse.APIKeys), ShouldEqual, 1)
So(apiKeyListResponse.APIKeys[0].CreatedAt, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatedAt)
So(apiKeyListResponse.APIKeys[0].CreatorUA, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatorUA)
So(apiKeyListResponse.APIKeys[0].Label, ShouldEqual, apiKeyResponse.APIKeyDetails.Label)
So(apiKeyListResponse.APIKeys[0].Scopes, ShouldEqual, apiKeyResponse.APIKeyDetails.Scopes)
So(apiKeyListResponse.APIKeys[0].UUID, ShouldEqual, apiKeyResponse.APIKeyDetails.UUID)
+ So(apiKeyListResponse.APIKeys[0].IsExpired, ShouldEqual, false)
+ So(apiKeyListResponse.APIKeys[0].ExpirationDate.Equal(expirationDate), ShouldBeTrue)
resp, err = client.R().
SetBasicAuth(email, apiKeyResponse.APIKey).
@@ -287,69 +2762,106 @@ func TestAPIKeys(t *testing.T) {
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- // trigger errors
- ctlr.MetaDB = mocks.MetaDBMock{
- GetUserAPIKeyInfoFn: func(hashedKey string) (string, error) {
- return "", ErrUnexpectedError
- },
- }
+ // sleep past expire time
+ time.Sleep(1500 * time.Millisecond)
resp, err = client.R().
SetBasicAuth(email, apiKeyResponse.APIKey).
Get(baseURL + "/v2/_catalog")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- ctlr.MetaDB = mocks.MetaDBMock{
- GetUserAPIKeyInfoFn: func(hashedKey string) (string, error) {
- return user.Email, nil
- },
- GetUserGroupsFn: func(ctx context.Context) ([]string, error) {
- return []string{}, ErrUnexpectedError
- },
- }
+ // again for coverage
+ resp, err = client.R().
+ SetBasicAuth(email, apiKeyResponse.APIKey).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ // get API key list with session authn
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Get(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
+ So(err, ShouldBeNil)
+ So(len(apiKeyListResponse.APIKeys), ShouldEqual, 1)
+ So(apiKeyListResponse.APIKeys[0].CreatedAt, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatedAt)
+ So(apiKeyListResponse.APIKeys[0].CreatorUA, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatorUA)
+ So(apiKeyListResponse.APIKeys[0].Label, ShouldEqual, apiKeyResponse.APIKeyDetails.Label)
+ So(apiKeyListResponse.APIKeys[0].Scopes, ShouldEqual, apiKeyResponse.APIKeyDetails.Scopes)
+ So(apiKeyListResponse.APIKeys[0].UUID, ShouldEqual, apiKeyResponse.APIKeyDetails.UUID)
+ So(apiKeyListResponse.APIKeys[0].IsExpired, ShouldEqual, true)
+ So(apiKeyListResponse.APIKeys[0].ExpirationDate.Equal(expirationDate), ShouldBeTrue)
+
+ // delete expired api key
+ resp, err = client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ SetQueryParam("id", apiKeyResponse.UUID).
+ Delete(baseURL + constants.APIKeyPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ // get API key list with session authn
resp, err = client.R().
- SetBasicAuth(email, apiKeyResponse.APIKey).
- Get(baseURL + "/v2/_catalog")
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Get(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- ctlr.MetaDB = mocks.MetaDBMock{
- GetUserAPIKeyInfoFn: func(hashedKey string) (string, error) {
- return user.Email, nil
- },
- UpdateUserAPIKeyLastUsedFn: func(ctx context.Context, hashedKey string) error {
- return ErrUnexpectedError
- },
+ err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
+ So(err, ShouldBeNil)
+ So(len(apiKeyListResponse.APIKeys), ShouldEqual, 0)
+ })
+
+ Convey("Create API key with expirationDate before actual date", func() {
+ expirationDate := time.Now().Add(-5 * time.Second).Local().Round(time.Second)
+ payload := api.APIKeyPayload{
+ Label: "test",
+ Scopes: []string{"test"},
+ ExpirationDate: expirationDate.Format(constants.APIKeyTimeFormat),
}
- resp, err = client.R().
- SetBasicAuth(email, apiKeyResponse.APIKey).
- Get(baseURL + "/v2/_catalog")
+ reqBody, err := json.Marshal(payload)
+ So(err, ShouldBeNil)
+
+ client := resty.New()
+
+ client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+ // first login user
+ resp, err := client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ SetQueryParam("provider", "oidc").
+ Get(baseURL + constants.LoginPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- client = resty.New()
+ client.SetCookies(resp.Cookies())
- // call endpoint without session
+ // call endpoint with session ( added to client after previous request)
resp, err = client.R().
SetBody(reqBody).
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
Post(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
})
- Convey("API key retrieved with openID and with long expire", func() {
+ Convey("Create API key with unparsable expirationDate", func() {
+ expirationDate := time.Now().Add(-5 * time.Second).Local().Round(time.Second)
payload := api.APIKeyPayload{
Label: "test",
Scopes: []string{"test"},
- ExpirationDate: time.Now().Add(time.Hour).Local().Format(constants.APIKeyTimeFormat),
+ ExpirationDate: expirationDate.Format(time.RFC1123Z),
}
reqBody, err := json.Marshal(payload)
@@ -357,15 +2869,9 @@ func TestAPIKeys(t *testing.T) {
client := resty.New()
- // mgmt should work both unauthenticated and authenticated
- resp, err := client.R().Get(baseURL + constants.FullMgmt)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
// first login user
- resp, err = client.R().
+ resp, err := client.R().
SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
SetQueryParam("provider", "oidc").
Get(baseURL + constants.LoginPath)
@@ -382,560 +2888,890 @@ func TestAPIKeys(t *testing.T) {
Post(baseURL + constants.APIKeyPath)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+ })
- var apiKeyResponse apiKeyResponse
- err = json.Unmarshal(resp.Body(), &apiKeyResponse)
- So(err, ShouldBeNil)
+ Convey("Test error handling when API Key handler reads the request body", func() {
+ request, _ := http.NewRequestWithContext(context.TODO(),
+ http.MethodPost, "baseURL", errReader(0))
+ response := httptest.NewRecorder()
- // get API key list
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Get(baseURL + constants.APIKeyPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ rthdlr := api.NewRouteHandler(ctlr)
+ rthdlr.CreateAPIKey(response, request)
- var apiKeyListResponse apiKeyListResponse
+ resp := response.Result()
+ defer resp.Body.Close()
+ So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
+ })
+ })
+}
- err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
- So(err, ShouldBeNil)
- So(len(apiKeyListResponse.APIKeys), ShouldEqual, 1)
- So(apiKeyListResponse.APIKeys[0].CreatedAt, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatedAt)
- So(apiKeyListResponse.APIKeys[0].CreatorUA, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatorUA)
- So(apiKeyListResponse.APIKeys[0].Label, ShouldEqual, apiKeyResponse.APIKeyDetails.Label)
- So(apiKeyListResponse.APIKeys[0].Scopes, ShouldEqual, apiKeyResponse.APIKeyDetails.Scopes)
- So(apiKeyListResponse.APIKeys[0].UUID, ShouldEqual, apiKeyResponse.APIKeyDetails.UUID)
+func TestAPIKeysOpenDBError(t *testing.T) {
+ Convey("Test API keys - unable to create database", t, func() {
+ conf := config.New()
+ htpasswdPath := test.MakeHtpasswdFile()
+ defer os.Remove(htpasswdPath)
- user := mockoidc.DefaultUser()
- email := user.Email
- So(email, ShouldNotBeEmpty)
+ mockOIDCServer, err := authutils.MockOIDCRun()
+ if err != nil {
+ panic(err)
+ }
- resp, err = client.R().
- SetBasicAuth(email, apiKeyResponse.APIKey).
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ defer func() {
+ err := mockOIDCServer.Shutdown()
+ if err != nil {
+ panic(err)
+ }
+ }()
- // auth with API key
- // we need new client without session cookie set
- client = resty.New()
- client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+ mockOIDCConfig := mockOIDCServer.Config()
+ defaultVal := true
- resp, err = client.R().
- SetBasicAuth(email, apiKeyResponse.APIKey).
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
- resp, err = client.R().
- SetBasicAuth(email, apiKeyResponse.APIKey).
- Get(baseURL + constants.FullMgmt)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ OpenID: &config.OpenIDConfig{
+ Providers: map[string]config.OpenIDProviderConfig{
+ "oidc": {
+ ClientID: mockOIDCConfig.ClientID,
+ ClientSecret: mockOIDCConfig.ClientSecret,
+ KeyPath: "",
+ Issuer: mockOIDCConfig.Issuer,
+ Scopes: []string{"openid", "email"},
+ },
+ },
+ },
- // get API key list
- resp, err = resty.R().
- SetBasicAuth(email, apiKeyResponse.APIKey).
- Get(baseURL + constants.APIKeyPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ APIKey: defaultVal,
+ }
- err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
- So(err, ShouldBeNil)
- So(len(apiKeyListResponse.APIKeys), ShouldEqual, 1)
+ ctlr := api.NewController(conf)
+ dir := t.TempDir()
- // invalid api keys
- resp, err = client.R().
- SetBasicAuth("invalidEmail", apiKeyResponse.APIKey).
- Get(baseURL + constants.FullMgmt)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ err = os.Chmod(dir, 0o000)
+ So(err, ShouldBeNil)
+
+ ctlr.Config.Storage.RootDirectory = dir
+ cm := test.NewControllerManager(ctlr)
+
+ So(func() {
+ cm.StartServer()
+ }, ShouldPanic)
+ })
+}
+
+func TestAPIKeysGeneratorErrors(t *testing.T) {
+ Convey("Test API keys - unable to generate API keys and API Key IDs", t, func() {
+ log := log.NewLogger("debug", "")
+
+ apiKey, apiKeyID, err := api.GenerateAPIKey(guuid.DefaultGenerator, log)
+ So(err, ShouldBeNil)
+ So(apiKey, ShouldNotEqual, "")
+ So(apiKeyID, ShouldNotEqual, "")
+
+ generator := &mockUUIDGenerator{
+ guuid.DefaultGenerator, 0, 0,
+ }
+
+ apiKey, apiKeyID, err = api.GenerateAPIKey(generator, log)
+ So(err, ShouldNotBeNil)
+ So(apiKey, ShouldEqual, "")
+ So(apiKeyID, ShouldEqual, "")
+
+ generator = &mockUUIDGenerator{
+ guuid.DefaultGenerator, 1, 0,
+ }
+
+ apiKey, apiKeyID, err = api.GenerateAPIKey(generator, log)
+ So(err, ShouldNotBeNil)
+ So(apiKey, ShouldEqual, "")
+ So(apiKeyID, ShouldEqual, "")
+ })
+}
+
+func TestHTTPReadOnly(t *testing.T) {
+ Convey("Single cred", t, func() {
+ singleCredtests := []string{}
+ user := ALICE
+ password := ALICE
+ singleCredtests = append(singleCredtests, getCredString(user, password))
+ singleCredtests = append(singleCredtests, getCredString(user, password)+"\n")
+
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+
+ for _, testString := range singleCredtests {
+ func() {
+ conf := config.New()
+ conf.HTTP.Port = port
+ // enable read-only mode
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Repositories: config.Repositories{
+ AuthorizationAllRepos: config.PolicyGroup{
+ DefaultPolicy: []string{"read"},
+ },
+ },
+ }
+
+ htpasswdPath := test.MakeHtpasswdFileFromString(testString)
+ defer os.Remove(htpasswdPath)
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ }
+ ctlr := makeController(conf, t.TempDir())
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ // with creds, should get expected status code
+ resp, _ := resty.R().SetBasicAuth(user, password).Get(baseURL + "/v2/")
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // with creds, any modifications should still fail on read-only mode
+ resp, err := resty.R().SetBasicAuth(user, password).
+ Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
+
+ // with invalid creds, it should fail
+ resp, _ = resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/")
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ }()
+ }
+ })
+}
+
+type mockUUIDGenerator struct {
+ guuid.Generator
+ succeedAttempts int
+ attemptCount int
+}
+
+func (gen *mockUUIDGenerator) NewV4() (
+ guuid.UUID, error,
+) {
+ defer func() {
+ gen.attemptCount += 1
+ }()
+
+ if gen.attemptCount >= gen.succeedAttempts {
+ return guuid.UUID{}, ErrUnexpectedError
+ }
+
+ return guuid.DefaultGenerator.NewV4()
+}
+
+type errReader int
+
+func (errReader) Read(p []byte) (int, error) {
+ return 0, fmt.Errorf("test error") //nolint:goerr113
+}
+
+func TestSearchRoutes(t *testing.T) {
+ Convey("Upload image for test", t, func(c C) {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ conf := config.New()
+ conf.HTTP.Port = port
+ tempDir := t.TempDir()
+
+ ctlr := makeController(conf, tempDir)
+ cm := test.NewControllerManager(ctlr)
- resp, err = client.R().
- SetBasicAuth(email, "noprefixAPIKey").
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- resp, err = client.R().
- SetBasicAuth(email, "zak_notworkingAPIKey").
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ repoName := "testrepo" //nolint:goconst
+ inaccessibleRepo := "inaccessible"
- userAc := reqCtx.NewUserAccessControl()
- userAc.SetUsername(email)
- ctx := userAc.DeriveContext(context.Background())
+ cfg, layers, manifest, err := deprecated.GetImageComponents(10000) //nolint:staticcheck
+ So(err, ShouldBeNil)
- err = ctlr.MetaDB.DeleteUserData(ctx)
- So(err, ShouldBeNil)
+ err = UploadImage(
+ Image{
+ Config: cfg,
+ Layers: layers,
+ Manifest: manifest,
+ }, baseURL, repoName, "latest")
- resp, err = client.R().
- SetBasicAuth(email, apiKeyResponse.APIKey).
- Get(baseURL + constants.FullMgmt)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
+ So(err, ShouldBeNil)
- client = resty.New()
- client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+ // data for the inaccessible repo
+ cfg, layers, manifest, err = deprecated.GetImageComponents(10000) //nolint:staticcheck
+ So(err, ShouldBeNil)
- // without creds should work
- resp, err = client.R().Get(baseURL + constants.FullMgmt)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ err = UploadImage(
+ Image{
+ Config: cfg,
+ Layers: layers,
+ Manifest: manifest,
+ }, baseURL, inaccessibleRepo, "latest")
- // login again
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- SetQueryParam("provider", "oidc").
- Get(baseURL + constants.LoginPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ So(err, ShouldBeNil)
- client.SetCookies(resp.Cookies())
+ Convey("GlobalSearch with authz enabled", func(c C) {
+ conf := config.New()
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+
+ user1 := "test"
+ password1 := "test"
+ testString1 := getCredString(user1, password1)
+ htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
+ defer os.Remove(htpasswdPath)
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ }
- resp, err = client.R().
- SetBody(reqBody).
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Post(baseURL + constants.APIKeyPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ conf.HTTP.Port = port
- err = json.Unmarshal(resp.Body(), &apiKeyResponse)
- So(err, ShouldBeNil)
+ defaultVal := true
- // should work with session
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Get(baseURL + constants.FullMgmt)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ searchConfig := &extconf.SearchConfig{
+ BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
+ }
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ conf.Extensions = &extconf.ExtensionConfig{
+ Search: searchConfig,
+ }
- // should work with api key
- resp, err = client.R().
- SetBasicAuth(email, apiKeyResponse.APIKey).
- Get(baseURL + constants.FullMgmt)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Repositories: config.Repositories{
+ repoName: config.PolicyGroup{
+ Policies: []config.Policy{
+ {
+ Users: []string{user1},
+ Actions: []string{"read", "create"},
+ },
+ },
+ DefaultPolicy: []string{},
+ },
+ inaccessibleRepo: config.PolicyGroup{
+ Policies: []config.Policy{
+ {
+ Users: []string{user1},
+ Actions: []string{"create"},
+ },
+ },
+ DefaultPolicy: []string{},
+ },
+ },
+ AdminPolicy: config.Policy{
+ Users: []string{},
+ Actions: []string{},
+ },
+ }
- resp, err = client.R().
- SetBasicAuth(email, apiKeyResponse.APIKey).
- Get(baseURL + "/v2/_catalog")
+ ctlr := makeController(conf, tempDir)
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ cfg, layers, manifest, err := deprecated.GetImageComponents(10000) //nolint:staticcheck
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- err = json.Unmarshal(resp.Body(), &apiKeyResponse)
+ err = UploadImageWithBasicAuth(
+ Image{
+ Config: cfg,
+ Layers: layers,
+ Manifest: manifest,
+ }, baseURL, repoName, "latest",
+ user1, password1)
So(err, ShouldBeNil)
- // delete api key
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- SetQueryParam("id", apiKeyResponse.UUID).
- Delete(baseURL + constants.APIKeyPath)
+ // data for the inaccessible repo
+ cfg, layers, manifest, err = deprecated.GetImageComponents(10000) //nolint:staticcheck
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- // apiKey removed, should get 401
- resp, err = client.R().
- SetBasicAuth(email, apiKeyResponse.APIKey).
- Get(baseURL + "/v2/_catalog")
+ err = UploadImageWithBasicAuth(
+ Image{
+ Config: cfg,
+ Layers: layers,
+ Manifest: manifest,
+ }, baseURL, inaccessibleRepo, "latest",
+ user1, password1)
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Delete(baseURL + constants.APIKeyPath)
+ query := `
+ {
+ GlobalSearch(query:"testrepo"){
+ Repos {
+ Name
+ NewestImage {
+ RepoName
+ Tag
+ }
+ }
+ }
+ }`
+ resp, err := resty.R().SetBasicAuth(user1, password1).Get(baseURL + constants.FullSearchPrefix +
+ "?query=" + url.QueryEscape(query))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+ So(resp.StatusCode(), ShouldEqual, 200)
+ So(string(resp.Body()), ShouldContainSubstring, repoName)
+ So(string(resp.Body()), ShouldNotContainSubstring, inaccessibleRepo)
- resp, err = client.R().
- SetBasicAuth(email, apiKeyResponse.APIKey).
- Get(baseURL + "/v2/_catalog")
+ resp, err = resty.R().Get(baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Repositories: config.Repositories{
+ repoName: config.PolicyGroup{
+ Policies: []config.Policy{
+ {
+ Users: []string{user1},
+ Actions: []string{},
+ },
+ },
+ DefaultPolicy: []string{},
+ },
+ inaccessibleRepo: config.PolicyGroup{
+ Policies: []config.Policy{
+ {
+ Users: []string{},
+ Actions: []string{},
+ },
+ },
+ DefaultPolicy: []string{},
+ },
+ },
+ AdminPolicy: config.Policy{
+ Users: []string{},
+ Actions: []string{},
+ },
+ }
- // get API key list
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Get(baseURL + constants.APIKeyPath)
+ // authenticated, but no access to resource
+ resp, err = resty.R().SetBasicAuth(user1, password1).Get(baseURL + constants.FullSearchPrefix +
+ "?query=" + url.QueryEscape(query))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ So(string(resp.Body()), ShouldNotContainSubstring, repoName)
+ So(string(resp.Body()), ShouldNotContainSubstring, inaccessibleRepo)
+ })
- err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
- So(err, ShouldBeNil)
- So(len(apiKeyListResponse.APIKeys), ShouldEqual, 0)
+ Convey("Testing group permissions", func(c C) {
+ conf := config.New()
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+
+ user1 := "test1"
+ password1 := "test1"
+ group1 := "testgroup3"
+ testString1 := getCredString(user1, password1)
+ htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
+ defer os.Remove(htpasswdPath)
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ }
- resp, err = client.R().
- SetBasicAuth("test", "test").
- SetQueryParam("id", apiKeyResponse.UUID).
- Delete(baseURL + constants.APIKeyPath)
+ conf.HTTP.Port = port
+
+ defaultVal := true
+
+ searchConfig := &extconf.SearchConfig{
+ BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
+ }
+
+ conf.Extensions = &extconf.ExtensionConfig{
+ Search: searchConfig,
+ }
+
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Groups: config.Groups{
+ group1: {
+ Users: []string{user1},
+ },
+ },
+ Repositories: config.Repositories{
+ repoName: config.PolicyGroup{
+ Policies: []config.Policy{
+ {
+ Groups: []string{group1},
+ Actions: []string{"read", "create"},
+ },
+ },
+ DefaultPolicy: []string{},
+ },
+ },
+ AdminPolicy: config.Policy{
+ Users: []string{},
+ Actions: []string{},
+ },
+ }
+
+ ctlr := makeController(conf, tempDir)
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ img := CreateRandomImage()
+
+ err = UploadImageWithBasicAuth(img, baseURL, repoName, img.DigestStr(), user1, password1)
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- // unsupported method
- resp, err = client.R().
- Put(baseURL + constants.APIKeyPath)
+ query := `
+ {
+ GlobalSearch(query:"testrepo"){
+ Repos {
+ Name
+ NewestImage {
+ RepoName
+ Tag
+ }
+ }
+ }
+ }`
+ resp, err := resty.R().SetBasicAuth(user1, password1).Get(baseURL + constants.FullSearchPrefix +
+ "?query=" + url.QueryEscape(query))
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
+ So(resp.StatusCode(), ShouldEqual, 200)
})
- Convey("API key retrieved with openID and with short expire", func() {
- expirationDate := time.Now().Add(1 * time.Second).Local().Round(time.Second)
- payload := api.APIKeyPayload{
- Label: "test",
- Scopes: []string{"test"},
- ExpirationDate: expirationDate.Format(constants.APIKeyTimeFormat),
+ Convey("Testing group permissions when the user is part of more groups with different permissions", func(c C) {
+ conf := config.New()
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+
+ user1 := "test2"
+ password1 := "test2"
+ group1 := "testgroup1"
+ group2 := "secondtestgroup"
+ testString1 := getCredString(user1, password1)
+ htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
+ defer os.Remove(htpasswdPath)
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
}
- reqBody, err := json.Marshal(payload)
- So(err, ShouldBeNil)
+ conf.HTTP.Port = port
- client := resty.New()
+ defaultVal := true
- client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
- // first login user
- resp, err := client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- SetQueryParam("provider", "oidc").
- Get(baseURL + constants.LoginPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ searchConfig := &extconf.SearchConfig{
+ BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
+ }
- client.SetCookies(resp.Cookies())
+ conf.Extensions = &extconf.ExtensionConfig{
+ Search: searchConfig,
+ }
- // call endpoint with session (added to client after previous request)
- resp, err = client.R().
- SetBody(reqBody).
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Post(baseURL + constants.APIKeyPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Groups: config.Groups{
+ group1: {
+ Users: []string{user1},
+ },
+ },
+ Repositories: config.Repositories{
+ repoName: config.PolicyGroup{
+ Policies: []config.Policy{
+ {
+ Groups: []string{group1},
+ Actions: []string{"delete"},
+ },
+ {
+ Groups: []string{group2},
+ Actions: []string{"read", "create"},
+ },
+ },
+ DefaultPolicy: []string{},
+ },
+ },
+ AdminPolicy: config.Policy{
+ Users: []string{},
+ Actions: []string{},
+ },
+ }
- var apiKeyResponse apiKeyResponse
- err = json.Unmarshal(resp.Body(), &apiKeyResponse)
- So(err, ShouldBeNil)
+ ctlr := makeController(conf, tempDir)
- user := mockoidc.DefaultUser()
- email := user.Email
- So(email, ShouldNotBeEmpty)
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- // get API key list
- resp, err = client.R().
- SetBasicAuth(email, apiKeyResponse.APIKey).
- Get(baseURL + constants.APIKeyPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ img := CreateRandomImage()
- var apiKeyListResponse apiKeyListResponse
+ err = UploadImageWithBasicAuth(img, baseURL, repoName, img.DigestStr(), user1, password1)
+ So(err, ShouldNotBeNil)
+ })
- err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
- So(err, ShouldBeNil)
- So(len(apiKeyListResponse.APIKeys), ShouldEqual, 1)
- So(apiKeyListResponse.APIKeys[0].CreatedAt, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatedAt)
- So(apiKeyListResponse.APIKeys[0].CreatorUA, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatorUA)
- So(apiKeyListResponse.APIKeys[0].Label, ShouldEqual, apiKeyResponse.APIKeyDetails.Label)
- So(apiKeyListResponse.APIKeys[0].Scopes, ShouldEqual, apiKeyResponse.APIKeyDetails.Scopes)
- So(apiKeyListResponse.APIKeys[0].UUID, ShouldEqual, apiKeyResponse.APIKeyDetails.UUID)
- So(apiKeyListResponse.APIKeys[0].IsExpired, ShouldEqual, false)
- So(apiKeyListResponse.APIKeys[0].ExpirationDate.Equal(expirationDate), ShouldBeTrue)
+ Convey("Testing group permissions when group has less permissions than user", func(c C) {
+ conf := config.New()
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+
+ user1 := "test3"
+ password1 := "test3"
+ group1 := "testgroup"
+ testString1 := getCredString(user1, password1)
+ htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
+ defer os.Remove(htpasswdPath)
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ }
- resp, err = client.R().
- SetBasicAuth(email, apiKeyResponse.APIKey).
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ conf.HTTP.Port = port
- // sleep past expire time
- time.Sleep(1500 * time.Millisecond)
+ defaultVal := true
- resp, err = client.R().
- SetBasicAuth(email, apiKeyResponse.APIKey).
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ searchConfig := &extconf.SearchConfig{
+ BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
+ }
- // again for coverage
- resp, err = client.R().
- SetBasicAuth(email, apiKeyResponse.APIKey).
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ conf.Extensions = &extconf.ExtensionConfig{
+ Search: searchConfig,
+ }
- // get API key list with session authn
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Get(baseURL + constants.APIKeyPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Groups: config.Groups{
+ group1: {
+ Users: []string{user1},
+ },
+ },
+ Repositories: config.Repositories{
+ repoName: config.PolicyGroup{
+ Policies: []config.Policy{
+ {
+ Groups: []string{group1},
+ Actions: []string{"delete"},
+ },
+ {
+ Users: []string{user1},
+ Actions: []string{"read", "create", "delete"},
+ },
+ },
+ DefaultPolicy: []string{},
+ },
+ },
+ AdminPolicy: config.Policy{
+ Users: []string{},
+ Actions: []string{},
+ },
+ }
- err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
- So(err, ShouldBeNil)
- So(len(apiKeyListResponse.APIKeys), ShouldEqual, 1)
- So(apiKeyListResponse.APIKeys[0].CreatedAt, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatedAt)
- So(apiKeyListResponse.APIKeys[0].CreatorUA, ShouldEqual, apiKeyResponse.APIKeyDetails.CreatorUA)
- So(apiKeyListResponse.APIKeys[0].Label, ShouldEqual, apiKeyResponse.APIKeyDetails.Label)
- So(apiKeyListResponse.APIKeys[0].Scopes, ShouldEqual, apiKeyResponse.APIKeyDetails.Scopes)
- So(apiKeyListResponse.APIKeys[0].UUID, ShouldEqual, apiKeyResponse.APIKeyDetails.UUID)
- So(apiKeyListResponse.APIKeys[0].IsExpired, ShouldEqual, true)
- So(apiKeyListResponse.APIKeys[0].ExpirationDate.Equal(expirationDate), ShouldBeTrue)
+ ctlr := makeController(conf, tempDir)
- // delete expired api key
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- SetQueryParam("id", apiKeyResponse.UUID).
- Delete(baseURL + constants.APIKeyPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- // get API key list with session authn
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Get(baseURL + constants.APIKeyPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ img := CreateRandomImage()
- err = json.Unmarshal(resp.Body(), &apiKeyListResponse)
+ err = UploadImageWithBasicAuth(img, baseURL, repoName, img.DigestStr(), user1, password1)
So(err, ShouldBeNil)
- So(len(apiKeyListResponse.APIKeys), ShouldEqual, 0)
})
- Convey("Create API key with expirationDate before actual date", func() {
- expirationDate := time.Now().Add(-5 * time.Second).Local().Round(time.Second)
- payload := api.APIKeyPayload{
- Label: "test",
- Scopes: []string{"test"},
- ExpirationDate: expirationDate.Format(constants.APIKeyTimeFormat),
+ Convey("Testing group permissions when user has less permissions than group", func(c C) {
+ conf := config.New()
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+
+ user1 := "test4"
+ password1 := "test4"
+ group1 := "testgroup1"
+ testString1 := getCredString(user1, password1)
+ htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
+ defer os.Remove(htpasswdPath)
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
}
- reqBody, err := json.Marshal(payload)
- So(err, ShouldBeNil)
+ conf.HTTP.Port = port
- client := resty.New()
+ defaultVal := true
- client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
- // first login user
- resp, err := client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- SetQueryParam("provider", "oidc").
- Get(baseURL + constants.LoginPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ searchConfig := &extconf.SearchConfig{
+ BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
+ }
- client.SetCookies(resp.Cookies())
+ conf.Extensions = &extconf.ExtensionConfig{
+ Search: searchConfig,
+ }
- // call endpoint with session ( added to client after previous request)
- resp, err = client.R().
- SetBody(reqBody).
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Post(baseURL + constants.APIKeyPath)
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Groups: config.Groups{
+ group1: {
+ Users: []string{user1},
+ },
+ },
+ Repositories: config.Repositories{
+ repoName: config.PolicyGroup{
+ Policies: []config.Policy{
+ {
+ Groups: []string{group1},
+ Actions: []string{"read", "create", "delete"},
+ },
+ {
+ Users: []string{user1},
+ Actions: []string{"delete"},
+ },
+ },
+ DefaultPolicy: []string{},
+ },
+ },
+ AdminPolicy: config.Policy{
+ Users: []string{},
+ Actions: []string{},
+ },
+ }
+
+ ctlr := makeController(conf, tempDir)
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ img := CreateRandomImage()
+
+ err = UploadImageWithBasicAuth(img, baseURL, repoName, img.DigestStr(), user1, password1)
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
})
- Convey("Create API key with unparsable expirationDate", func() {
- expirationDate := time.Now().Add(-5 * time.Second).Local().Round(time.Second)
- payload := api.APIKeyPayload{
- Label: "test",
- Scopes: []string{"test"},
- ExpirationDate: expirationDate.Format(time.RFC1123Z),
+ Convey("Testing group permissions on admin policy", func(c C) {
+ conf := config.New()
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+
+ user1 := "test5"
+ password1 := "test5"
+ group1 := "testgroup2"
+ testString1 := getCredString(user1, password1)
+ htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
+ defer os.Remove(htpasswdPath)
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
}
- reqBody, err := json.Marshal(payload)
- So(err, ShouldBeNil)
+ conf.HTTP.Port = port
- client := resty.New()
+ defaultVal := true
- client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
- // first login user
- resp, err := client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- SetQueryParam("provider", "oidc").
- Get(baseURL + constants.LoginPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ searchConfig := &extconf.SearchConfig{
+ BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
+ }
- client.SetCookies(resp.Cookies())
+ conf.Extensions = &extconf.ExtensionConfig{
+ Search: searchConfig,
+ }
- // call endpoint with session ( added to client after previous request)
- resp, err = client.R().
- SetBody(reqBody).
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Post(baseURL + constants.APIKeyPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
- })
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Groups: config.Groups{
+ group1: {
+ Users: []string{user1},
+ },
+ },
+ Repositories: config.Repositories{},
+ AdminPolicy: config.Policy{
+ Groups: []string{group1},
+ Actions: []string{"read", "create"},
+ },
+ }
- Convey("Test error handling when API Key handler reads the request body", func() {
- request, _ := http.NewRequestWithContext(context.TODO(),
- http.MethodPost, "baseURL", errReader(0))
- response := httptest.NewRecorder()
+ ctlr := makeController(conf, tempDir)
- rthdlr := api.NewRouteHandler(ctlr)
- rthdlr.CreateAPIKey(response, request)
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- resp := response.Result()
- defer resp.Body.Close()
- So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
+ img := CreateRandomImage()
+
+ err = UploadImageWithBasicAuth(img, baseURL, repoName, img.DigestStr(), user1, password1)
+ So(err, ShouldBeNil)
})
- })
-}
-func TestAPIKeysOpenDBError(t *testing.T) {
- Convey("Test API keys - unable to create database", t, func() {
- conf := config.New()
- htpasswdPath := test.MakeHtpasswdFile()
- defer os.Remove(htpasswdPath)
+ Convey("Testing group permissions on anonymous policy", func(c C) {
+ conf := config.New()
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
- mockOIDCServer, err := authutils.MockOIDCRun()
- if err != nil {
- panic(err)
- }
+ conf.HTTP.Port = port
- defer func() {
- err := mockOIDCServer.Shutdown()
- if err != nil {
- panic(err)
- }
- }()
+ defaultVal := true
+ group1 := group
+ user1 := username
+ password1 := passphrase
- mockOIDCConfig := mockOIDCServer.Config()
- defaultVal := true
+ testString1 := getCredString(user1, password1)
+ htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
+ defer os.Remove(htpasswdPath)
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ }
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
+ searchConfig := &extconf.SearchConfig{
+ BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
+ }
- OpenID: &config.OpenIDConfig{
- Providers: map[string]config.OpenIDProviderConfig{
- "oidc": {
- ClientID: mockOIDCConfig.ClientID,
- ClientSecret: mockOIDCConfig.ClientSecret,
- KeyPath: "",
- Issuer: mockOIDCConfig.Issuer,
- Scopes: []string{"openid", "email"},
+ conf.Extensions = &extconf.ExtensionConfig{
+ Search: searchConfig,
+ }
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Groups: config.Groups{
+ group1: {
+ Users: []string{user1},
},
},
- },
-
- APIKey: defaultVal,
- }
+ Repositories: config.Repositories{
+ repoName: config.PolicyGroup{
+ Policies: []config.Policy{
+ {
+ Groups: []string{group1},
+ Actions: []string{"read", "create", "delete"},
+ },
+ {
+ Users: []string{user1},
+ Actions: []string{"delete"},
+ },
+ },
+ DefaultPolicy: []string{},
+ AnonymousPolicy: []string{"read", "create"},
+ },
+ },
+ }
- ctlr := api.NewController(conf)
- dir := t.TempDir()
+ ctlr := makeController(conf, tempDir)
- err = os.Chmod(dir, 0o000)
- So(err, ShouldBeNil)
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- ctlr.Config.Storage.RootDirectory = dir
- cm := test.NewControllerManager(ctlr)
+ img := CreateRandomImage()
- So(func() {
- cm.StartServer()
- }, ShouldPanic)
+ err = UploadImageWithBasicAuth(img, baseURL, repoName, img.DigestStr(), "", "")
+ So(err, ShouldBeNil)
+ })
})
}
-func TestAPIKeysGeneratorErrors(t *testing.T) {
- Convey("Test API keys - unable to generate API keys and API Key IDs", t, func() {
- log := log.NewLogger("debug", "")
+func TestMetricsAuthentication(t *testing.T) {
+ Convey("test metrics without authentication and metrics enabled", t, func() {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ conf := config.New()
+ conf.HTTP.Port = port
- apiKey, apiKeyID, err := api.GenerateAPIKey(guuid.DefaultGenerator, log)
- So(err, ShouldBeNil)
- So(apiKey, ShouldNotEqual, "")
- So(apiKeyID, ShouldNotEqual, "")
+ ctlr := api.NewController(conf)
+ ctlr.Config.Storage.RootDirectory = t.TempDir()
- generator := &mockUUIDGenerator{
- guuid.DefaultGenerator, 0, 0,
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ // metrics endpoint not available
+ resp, err := resty.R().Get(baseURL + "/metrics")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ })
+ Convey("test metrics without authentication and with metrics enabled", t, func() {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ conf := config.New()
+ conf.HTTP.Port = port
+ enabled := true
+ metricsConfig := &extconf.MetricsConfig{
+ BaseConfig: extconf.BaseConfig{Enable: &enabled},
+ Prometheus: &extconf.PrometheusConfig{Path: "/metrics"},
+ }
+ conf.Extensions = &extconf.ExtensionConfig{
+ Metrics: metricsConfig,
}
- apiKey, apiKeyID, err = api.GenerateAPIKey(generator, log)
- So(err, ShouldNotBeNil)
- So(apiKey, ShouldEqual, "")
- So(apiKeyID, ShouldEqual, "")
+ ctlr := api.NewController(conf)
+ ctlr.Config.Storage.RootDirectory = t.TempDir()
- generator = &mockUUIDGenerator{
- guuid.DefaultGenerator, 1, 0,
- }
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- apiKey, apiKeyID, err = api.GenerateAPIKey(generator, log)
- So(err, ShouldNotBeNil)
- So(apiKey, ShouldEqual, "")
- So(apiKeyID, ShouldEqual, "")
+ // without auth set metrics endpoint is available
+ resp, err := resty.R().Get(baseURL + "/metrics")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
})
-}
+ Convey("test metrics with authentication and metrics enabled", t, func() {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ conf := config.New()
+ conf.HTTP.Port = port
-type mockUUIDGenerator struct {
- guuid.Generator
- succeedAttempts int
- attemptCount int
-}
+ credTests := fmt.Sprintf("%s\n%s\n", getCredString(username, passphrase), getCredString(metricsuser, metricspass))
+ htpasswdPath := test.MakeHtpasswdFileFromString(credTests)
-func (gen *mockUUIDGenerator) NewV4() (
- guuid.UUID, error,
-) {
- defer func() {
- gen.attemptCount += 1
- }()
+ defer os.Remove(htpasswdPath)
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ }
- if gen.attemptCount >= gen.succeedAttempts {
- return guuid.UUID{}, ErrUnexpectedError
- }
+ enabled := true
+ metricsConfig := &extconf.MetricsConfig{
+ BaseConfig: extconf.BaseConfig{Enable: &enabled},
+ Prometheus: &extconf.PrometheusConfig{Path: "/metrics"},
+ }
+ conf.Extensions = &extconf.ExtensionConfig{
+ Metrics: metricsConfig,
+ }
- return guuid.DefaultGenerator.NewV4()
-}
+ ctlr := api.NewController(conf)
+ ctlr.Config.Storage.RootDirectory = t.TempDir()
-type errReader int
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
-func (errReader) Read(p []byte) (int, error) {
- return 0, fmt.Errorf("test error") //nolint:goerr113
+ // without credentials
+ resp, err := resty.R().Get(baseURL + "/metrics")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ // with wrong credentials
+ resp, err = resty.R().SetBasicAuth("atacker", "wrongpassword").Get(baseURL + "/metrics")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ // authenticated users
+ resp, err = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + "/metrics")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ resp, err = resty.R().SetBasicAuth(metricsuser, metricspass).Get(baseURL + "/metrics")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ })
}
diff --git a/pkg/api/authz.go b/pkg/api/authz.go
index 3228e8b038..42c972478b 100644
--- a/pkg/api/authz.go
+++ b/pkg/api/authz.go
@@ -191,14 +191,10 @@ func (ac *AccessController) getAuthnMiddlewareContext(authnType string, request
func (ac *AccessController) isPermitted(userGroups []string, username, action string,
policyGroup config.PolicyGroup,
) bool {
- var result bool
-
// check repo/system based policies
for _, p := range policyGroup.Policies {
if common.Contains(p.Users, username) && common.Contains(p.Actions, action) {
- result = true
-
- return result
+ return true
}
}
@@ -207,9 +203,7 @@ func (ac *AccessController) isPermitted(userGroups []string, username, action st
if common.Contains(p.Actions, action) {
for _, group := range p.Groups {
if common.Contains(userGroups, group) {
- result = true
-
- return result
+ return true
}
}
}
@@ -217,20 +211,16 @@ func (ac *AccessController) isPermitted(userGroups []string, username, action st
}
// check defaultPolicy
- if !result {
- if common.Contains(policyGroup.DefaultPolicy, action) && username != "" {
- result = true
- }
+ if common.Contains(policyGroup.DefaultPolicy, action) && username != "" {
+ return true
}
// check anonymousPolicy
- if !result {
- if common.Contains(policyGroup.AnonymousPolicy, action) && username == "" {
- result = true
- }
+ if common.Contains(policyGroup.AnonymousPolicy, action) && username == "" {
+ return true
}
- return result
+ return false
}
func BaseAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
@@ -343,3 +333,40 @@ func DistSpecAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
})
}
}
+
+func MetricsAuthzHandler(ctlr *Controller) mux.MiddlewareFunc {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
+ if ctlr.Config.HTTP.AccessControl == nil {
+ // allow access to authenticated user as anonymous policy does not exist
+ next.ServeHTTP(response, request)
+
+ return
+ }
+ if len(ctlr.Config.HTTP.AccessControl.Metrics.Users) == 0 {
+ log := ctlr.Log
+ log.Warn().Msg("auth is enabled but no metrics users in accessControl: /metrics is unaccesible")
+ common.AuthzFail(response, request, "", ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
+
+ return
+ }
+
+ // get access control context made in authn.go
+ userAc, err := reqCtx.UserAcFromContext(request.Context())
+ if err != nil { // should never happen
+ common.AuthzFail(response, request, "", ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
+
+ return
+ }
+
+ username := userAc.GetUsername()
+ if !common.Contains(ctlr.Config.HTTP.AccessControl.Metrics.Users, username) {
+ common.AuthzFail(response, request, username, ctlr.Config.HTTP.Realm, ctlr.Config.HTTP.Auth.FailDelay)
+
+ return
+ }
+
+ next.ServeHTTP(response, request) //nolint:contextcheck
+ })
+ }
+}
diff --git a/pkg/api/authz_test.go b/pkg/api/authz_test.go
new file mode 100644
index 0000000000..54663f945e
--- /dev/null
+++ b/pkg/api/authz_test.go
@@ -0,0 +1,1594 @@
+//go:build sync && scrub && metrics && search && lint && userprefs && mgmt && imagetrust && ui
+// +build sync,scrub,metrics,search,lint,userprefs,mgmt,imagetrust,ui
+
+package api_test
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+ "path"
+ "testing"
+
+ godigest "github.com/opencontainers/go-digest"
+ ispec "github.com/opencontainers/image-spec/specs-go/v1"
+ "github.com/project-zot/mockoidc"
+ . "github.com/smartystreets/goconvey/convey"
+ "gopkg.in/resty.v1"
+
+ "zotregistry.io/zot/pkg/api"
+ "zotregistry.io/zot/pkg/api/config"
+ "zotregistry.io/zot/pkg/api/constants"
+ apiErr "zotregistry.io/zot/pkg/api/errors"
+ extconf "zotregistry.io/zot/pkg/extensions/config"
+ storageConstants "zotregistry.io/zot/pkg/storage/constants"
+ authutils "zotregistry.io/zot/pkg/test/auth"
+ test "zotregistry.io/zot/pkg/test/common"
+ . "zotregistry.io/zot/pkg/test/image-utils"
+ ociutils "zotregistry.io/zot/pkg/test/oci-utils"
+)
+
+const (
+ AuthorizationNamespace = "authz/image"
+ metricsuser = "metrics"
+ metricspass = "metrics"
+)
+
+func TestAuthorization(t *testing.T) {
+ Convey("Make a new controller", t, func() {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ conf := config.New()
+ conf.HTTP.Port = port
+ htpasswdPath := test.MakeHtpasswdFile()
+ defer os.Remove(htpasswdPath)
+
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ }
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Repositories: config.Repositories{
+ AuthorizationAllRepos: config.PolicyGroup{
+ Policies: []config.Policy{
+ {
+ Users: []string{},
+ Actions: []string{},
+ },
+ },
+ DefaultPolicy: []string{},
+ },
+ },
+ AdminPolicy: config.Policy{
+ Users: []string{},
+ Actions: []string{},
+ },
+ }
+
+ Convey("with openid", func() {
+ mockOIDCServer, err := authutils.MockOIDCRun()
+ if err != nil {
+ panic(err)
+ }
+
+ defer func() {
+ err := mockOIDCServer.Shutdown()
+ if err != nil {
+ panic(err)
+ }
+ }()
+
+ mockOIDCConfig := mockOIDCServer.Config()
+ conf.HTTP.Auth = &config.AuthConfig{
+ OpenID: &config.OpenIDConfig{
+ Providers: map[string]config.OpenIDProviderConfig{
+ "oidc": {
+ ClientID: mockOIDCConfig.ClientID,
+ ClientSecret: mockOIDCConfig.ClientSecret,
+ KeyPath: "",
+ Issuer: mockOIDCConfig.Issuer,
+ Scopes: []string{"openid", "email"},
+ },
+ },
+ },
+ }
+
+ ctlr := api.NewController(conf)
+ ctlr.Config.Storage.RootDirectory = t.TempDir()
+
+ err = WriteImageToFileSystem(CreateDefaultImage(), "zot-test", "0.0.1",
+ ociutils.GetDefaultStoreController(ctlr.Config.Storage.RootDirectory, ctlr.Log))
+ So(err, ShouldBeNil)
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ client := resty.New()
+
+ client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+
+ mockOIDCServer.QueueUser(&mockoidc.MockUser{
+ Email: "test",
+ Subject: "1234567890",
+ })
+
+ // first login user
+ resp, err := client.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ SetQueryParam("provider", "oidc").
+ Get(baseURL + constants.LoginPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ client.SetCookies(resp.Cookies())
+ client.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
+
+ RunAuthorizationTests(t, client, baseURL, conf)
+ })
+
+ Convey("with basic auth", func() {
+ ctlr := api.NewController(conf)
+ ctlr.Config.Storage.RootDirectory = t.TempDir()
+
+ err := WriteImageToFileSystem(CreateDefaultImage(), "zot-test", "0.0.1",
+ ociutils.GetDefaultStoreController(ctlr.Config.Storage.RootDirectory, ctlr.Log))
+ So(err, ShouldBeNil)
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ client := resty.New()
+ client.SetBasicAuth(username, passphrase)
+
+ RunAuthorizationTests(t, client, baseURL, conf)
+ })
+ })
+}
+
+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, http.StatusUnauthorized)
+
+ // 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 := 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)
+ })
+}
+
+func TestAuthorizationWithOnlyAnonymousPolicy(t *testing.T) {
+ Convey("Make a new controller", t, func() {
+ const TestRepo = "my-repos/repo"
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+
+ conf := config.New()
+ conf.HTTP.Port = port
+ conf.HTTP.Auth = &config.AuthConfig{}
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Repositories: config.Repositories{
+ TestRepo: config.PolicyGroup{
+ AnonymousPolicy: []string{"read"},
+ },
+ },
+ }
+
+ dir := t.TempDir()
+ ctlr := makeController(conf, dir)
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ blob := []byte("hello, blob!")
+ digest := godigest.FromBytes(blob).String()
+
+ resp, err := resty.R().Get(baseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ resp, err = resty.R().Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ var e apiErr.Error
+ err = json.Unmarshal(resp.Body(), &e)
+ So(err, ShouldBeNil)
+
+ // 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.StatusUnauthorized)
+
+ if entry, ok := conf.HTTP.AccessControl.Repositories[TestRepo]; ok {
+ entry.AnonymousPolicy = []string{"create", "read"}
+ conf.HTTP.AccessControl.Repositories[TestRepo] = entry
+ }
+
+ // now it should get 202
+ resp, err = resty.R().Post(baseURL + "/v2/" + TestRepo + "/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().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)
+
+ cblob, cdigest := GetRandomImageConfig()
+
+ resp, err = resty.R().Post(baseURL + "/v2/" + TestRepo + "/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().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)
+
+ manifest := 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(blob),
+ Size: int64(len(blob)),
+ },
+ },
+ }
+ manifest.SchemaVersion = 2
+ manifestBlob, err := json.Marshal(manifest)
+ So(err, ShouldBeNil)
+
+ resp, err = resty.R().
+ SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
+ SetBody(manifestBlob).
+ Put(baseURL + "/v2/" + TestRepo + "/manifests/0.0.2")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ updateBlob := []byte("Hello, blob update!")
+
+ resp, err = resty.R().
+ Post(baseURL + "/v2/" + TestRepo + "/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().
+ 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 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.StatusUnauthorized)
+
+ // get the manifest and check if it's the old one
+ resp, err = resty.R().
+ Get(baseURL + "/v2/" + TestRepo + "/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
+ if entry, ok := conf.HTTP.AccessControl.Repositories[TestRepo]; ok {
+ entry.AnonymousPolicy = []string{"create", "read", "update"}
+ conf.HTTP.AccessControl.Repositories[TestRepo] = entry
+ }
+
+ // update manifest should get 201 with update perm
+ resp, err = resty.R().
+ SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
+ SetBody(updatedManifestBlob).
+ Put(baseURL + "/v2/" + TestRepo + "/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().
+ Get(baseURL + "/v2/" + TestRepo + "/manifests/0.0.2")
+ So(err, ShouldBeNil)
+ 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)
+
+ err = WriteImageToFileSystem(CreateDefaultImage(), "zot-test", "tag", ctlr.StoreController)
+ So(err, ShouldBeNil)
+
+ // 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")
+ })
+}
+
+func TestAuthorizationWithAnonymousPolicyBasicAuthAndSessionHeader(t *testing.T) {
+ Convey("Make a new controller", t, func() {
+ const TestRepo = "my-repos/repo"
+ const AllRepos = "**"
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+
+ badpassphrase := "bad"
+ htpasswdContent := fmt.Sprintf("%s:$2y$05$hlbSXDp6hzDLu6VwACS39ORvVRpr3OMR4RlJ31jtlaOEGnPjKZI1m\n",
+ htpasswdUsername)
+
+ htpasswdPath := test.MakeHtpasswdFileFromString(htpasswdContent)
+ defer os.Remove(htpasswdPath)
+
+ img := CreateRandomImage()
+ tagAnonymous := "1.0-anon"
+ tagAuth := "1.0-auth"
+ tagUnauth := "1.0-unauth"
+
+ conf := config.New()
+ conf.HTTP.Port = port
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ }
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Repositories: config.Repositories{
+ AllRepos: config.PolicyGroup{
+ Policies: []config.Policy{
+ {
+ Users: []string{htpasswdUsername},
+ Actions: []string{"read"},
+ },
+ },
+ AnonymousPolicy: []string{"read"},
+ },
+ },
+ }
+
+ dir := t.TempDir()
+ ctlr := makeController(conf, dir)
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ // /v2 access
+ // Can access /v2 without credentials
+ resp, err := resty.R().Get(baseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // Can access /v2 without credentials and with X-Zot-Api-Client=zot-ui
+ resp, err = resty.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Get(baseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // Can access /v2 with correct credentials
+ resp, err = resty.R().
+ SetBasicAuth(htpasswdUsername, passphrase).
+ Get(baseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // Fail to access /v2 with incorrect credentials
+ resp, err = resty.R().
+ SetBasicAuth(htpasswdUsername, badpassphrase).
+ Get(baseURL + "/v2/")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ // Catalog access
+ resp, err = resty.R().Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ var apiError apiErr.Error
+ err = json.Unmarshal(resp.Body(), &apiError)
+ So(err, ShouldBeNil)
+
+ resp, err = resty.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ apiError = apiErr.Error{}
+ err = json.Unmarshal(resp.Body(), &apiError)
+ So(err, ShouldBeNil)
+
+ resp, err = resty.R().
+ SetBasicAuth(htpasswdUsername, passphrase).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ apiError = apiErr.Error{}
+ err = json.Unmarshal(resp.Body(), &apiError)
+ So(err, ShouldBeNil)
+
+ resp, err = resty.R().
+ SetBasicAuth(htpasswdUsername, badpassphrase).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ apiError = apiErr.Error{}
+ err = json.Unmarshal(resp.Body(), &apiError)
+ So(err, ShouldBeNil)
+
+ // upload capability
+ // should get 403 without create
+ err = UploadImage(img, baseURL, TestRepo, tagAnonymous)
+ So(err, ShouldNotBeNil)
+
+ err = UploadImageWithBasicAuth(img, baseURL,
+ TestRepo, tagAuth, htpasswdUsername, passphrase)
+ So(err, ShouldNotBeNil)
+
+ err = UploadImageWithBasicAuth(img, baseURL,
+ TestRepo, tagUnauth, htpasswdUsername, badpassphrase)
+ So(err, ShouldNotBeNil)
+
+ if entry, ok := conf.HTTP.AccessControl.Repositories[AllRepos]; ok {
+ entry.AnonymousPolicy = []string{"create", "read"}
+ entry.Policies[0] = config.Policy{
+ Users: []string{htpasswdUsername},
+ Actions: []string{"create", "read"},
+ }
+ conf.HTTP.AccessControl.Repositories[AllRepos] = entry
+ }
+
+ // now it should succeed for valid users
+ err = UploadImage(img, baseURL, TestRepo, tagAnonymous)
+ So(err, ShouldBeNil)
+
+ err = UploadImageWithBasicAuth(img, baseURL,
+ TestRepo, tagAuth, htpasswdUsername, passphrase)
+ So(err, ShouldBeNil)
+
+ err = UploadImageWithBasicAuth(img, baseURL,
+ TestRepo, tagUnauth, htpasswdUsername, badpassphrase)
+ So(err, ShouldNotBeNil)
+
+ // read capability
+ catalog := struct {
+ Repositories []string `json:"repositories"`
+ }{}
+
+ 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)
+
+ catalog = struct {
+ Repositories []string `json:"repositories"`
+ }{}
+
+ resp, err = resty.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ 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)
+
+ catalog = struct {
+ Repositories []string `json:"repositories"`
+ }{}
+
+ resp, err = resty.R().
+ SetBasicAuth(htpasswdUsername, passphrase).
+ 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)
+
+ resp, err = resty.R().
+ SetBasicAuth(htpasswdUsername, badpassphrase).
+ Get(baseURL + "/v2/_catalog")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ })
+}
+
+func TestAuthorizationWithMultiplePolicies(t *testing.T) {
+ Convey("Make a new controller", t, func() {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+
+ conf := config.New()
+ conf.HTTP.Port = port
+ // have two users: "test" user for user Policy, and "bob" for default policy
+ htpasswdPath := test.MakeHtpasswdFileFromString(getCredString(username, passphrase) +
+ "\n" + getCredString("bob", passphrase))
+ defer os.Remove(htpasswdPath)
+
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ }
+ // config with all policy types, to test that the correct one is applied in each case
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Repositories: config.Repositories{
+ AuthorizationAllRepos: config.PolicyGroup{
+ Policies: []config.Policy{
+ {
+ Users: []string{},
+ Actions: []string{},
+ },
+ },
+ DefaultPolicy: []string{},
+ AnonymousPolicy: []string{},
+ },
+ },
+ AdminPolicy: config.Policy{
+ Users: []string{},
+ Actions: []string{},
+ },
+ }
+
+ Convey("with openid", func() {
+ dir := t.TempDir()
+
+ mockOIDCServer, err := authutils.MockOIDCRun()
+ if err != nil {
+ panic(err)
+ }
+
+ defer func() {
+ err := mockOIDCServer.Shutdown()
+ if err != nil {
+ panic(err)
+ }
+ }()
+
+ mockOIDCConfig := mockOIDCServer.Config()
+ conf.HTTP.Auth = &config.AuthConfig{
+ OpenID: &config.OpenIDConfig{
+ Providers: map[string]config.OpenIDProviderConfig{
+ "oidc": {
+ ClientID: mockOIDCConfig.ClientID,
+ ClientSecret: mockOIDCConfig.ClientSecret,
+ KeyPath: "",
+ Issuer: mockOIDCConfig.Issuer,
+ Scopes: []string{"openid", "email"},
+ },
+ },
+ },
+ }
+
+ ctlr := api.NewController(conf)
+ ctlr.Config.Storage.RootDirectory = dir
+
+ err = WriteImageToFileSystem(CreateDefaultImage(), "zot-test", "0.0.1",
+ ociutils.GetDefaultStoreController(ctlr.Config.Storage.RootDirectory, ctlr.Log))
+ So(err, ShouldBeNil)
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ testUserClient := resty.New()
+
+ testUserClient.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+
+ mockOIDCServer.QueueUser(&mockoidc.MockUser{
+ Email: "test",
+ Subject: "1234567890",
+ })
+
+ // first login user
+ resp, err := testUserClient.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ SetQueryParam("provider", "oidc").
+ Get(baseURL + constants.LoginPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ testUserClient.SetCookies(resp.Cookies())
+ testUserClient.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
+
+ bobUserClient := resty.New()
+
+ bobUserClient.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+
+ mockOIDCServer.QueueUser(&mockoidc.MockUser{
+ Email: "bob",
+ Subject: "1234567890",
+ })
+
+ // first login user
+ resp, err = bobUserClient.R().
+ SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
+ SetQueryParam("provider", "oidc").
+ Get(baseURL + constants.LoginPath)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ bobUserClient.SetCookies(resp.Cookies())
+ bobUserClient.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
+
+ RunAuthorizationWithMultiplePoliciesTests(t, testUserClient, bobUserClient, baseURL, conf)
+ })
+
+ Convey("with basic auth", func() {
+ dir := t.TempDir()
+
+ ctlr := api.NewController(conf)
+ ctlr.Config.Storage.RootDirectory = dir
+
+ err := WriteImageToFileSystem(CreateDefaultImage(), "zot-test", "0.0.1",
+ ociutils.GetDefaultStoreController(ctlr.Config.Storage.RootDirectory, ctlr.Log))
+ So(err, ShouldBeNil)
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ testUserClient := resty.New()
+ testUserClient.SetBasicAuth(username, passphrase)
+
+ bobUserClient := resty.New()
+ bobUserClient.SetBasicAuth("bob", passphrase)
+
+ RunAuthorizationWithMultiplePoliciesTests(t, testUserClient, bobUserClient, baseURL, conf)
+ })
+ })
+}
+
+func TestMetricsAuthorization(t *testing.T) {
+ Convey("Make a new controller with auth & metrics enabled", t, func() {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ conf := config.New()
+ conf.HTTP.Port = port
+ credTests := fmt.Sprintf("%s\n%s\n", getCredString(username, passphrase), getCredString(metricsuser, metricspass))
+ htpasswdPath := test.MakeHtpasswdFileFromString(credTests)
+ defer os.Remove(htpasswdPath)
+
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ }
+
+ enabled := true
+ metricsConfig := &extconf.MetricsConfig{
+ BaseConfig: extconf.BaseConfig{Enable: &enabled},
+ Prometheus: &extconf.PrometheusConfig{Path: "/metrics"},
+ }
+ conf.Extensions = &extconf.ExtensionConfig{
+ Metrics: metricsConfig,
+ }
+
+ Convey("with basic auth: no metrics users in accessControl", func() {
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Metrics: config.Metrics{
+ Users: []string{},
+ },
+ }
+ ctlr := api.NewController(conf)
+ ctlr.Config.Storage.RootDirectory = t.TempDir()
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ // authenticated but not authorized user should not have access to/metrics
+ client := resty.New()
+ client.SetBasicAuth(username, passphrase)
+ resp, err := client.R().Get(baseURL + "/metrics")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ // authenticated but not authorized user should not have access to/metrics
+ client.SetBasicAuth(metricsuser, metricspass)
+ resp, err = client.R().Get(baseURL + "/metrics")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ })
+ Convey("with basic auth: metrics users in accessControl", func() {
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Metrics: config.Metrics{
+ Users: []string{metricsuser},
+ },
+ }
+ ctlr := api.NewController(conf)
+ ctlr.Config.Storage.RootDirectory = t.TempDir()
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ // authenticated but not authorized user should not have access to/metrics
+ client := resty.New()
+ client.SetBasicAuth(username, passphrase)
+ resp, err := client.R().Get(baseURL + "/metrics")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
+
+ // authenticated & authorized user should have access to/metrics
+ client.SetBasicAuth(metricsuser, metricspass)
+ resp, err = client.R().Get(baseURL + "/metrics")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ })
+ Convey("with basic auth: with anonymousPolicy in accessControl", func() {
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Metrics: config.Metrics{
+ Users: []string{metricsuser},
+ },
+ Repositories: config.Repositories{
+ AuthorizationAllRepos: config.PolicyGroup{
+ Policies: []config.Policy{
+ {
+ Users: []string{},
+ Actions: []string{},
+ },
+ },
+ AnonymousPolicy: []string{"read"},
+ DefaultPolicy: []string{},
+ },
+ },
+ }
+ ctlr := api.NewController(conf)
+ ctlr.Config.Storage.RootDirectory = t.TempDir()
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ // unauthenticated clients should not have access to /metrics
+ resp, err := resty.R().Get(baseURL + "/metrics")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ // unauthenticated clients should not have access to /metrics
+ resp, err = resty.R().SetBasicAuth("hacker", "trywithwrongpass").Get(baseURL + "/metrics")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ // authenticated but not authorized user should not have access to/metrics
+ client := resty.New()
+ client.SetBasicAuth(username, passphrase)
+ resp, err = client.R().Get(baseURL + "/metrics")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
+
+ // authenticated & authorized user should have access to/metrics
+ client.SetBasicAuth(metricsuser, metricspass)
+ resp, err = client.R().Get(baseURL + "/metrics")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ })
+ Convey("with basic auth: with adminPolicy in accessControl", func() {
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Metrics: config.Metrics{
+ Users: []string{metricsuser},
+ },
+ Repositories: config.Repositories{
+ AuthorizationAllRepos: config.PolicyGroup{
+ Policies: []config.Policy{
+ {
+ Users: []string{},
+ Actions: []string{},
+ },
+ },
+ DefaultPolicy: []string{},
+ },
+ },
+ AdminPolicy: config.Policy{
+ Users: []string{"test"},
+ Groups: []string{"admins"},
+ Actions: []string{"read", "create", "update", "delete"},
+ },
+ }
+ ctlr := api.NewController(conf)
+ ctlr.Config.Storage.RootDirectory = t.TempDir()
+
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ // unauthenticated clients should not have access to /metrics
+ resp, err := resty.R().Get(baseURL + "/metrics")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ // unauthenticated clients should not have access to /metrics
+ resp, err = resty.R().SetBasicAuth("hacker", "trywithwrongpass").Get(baseURL + "/metrics")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+
+ // authenticated admin user (but not authorized) should not have access to/metrics
+ client := resty.New()
+ client.SetBasicAuth(username, passphrase)
+ resp, err = client.R().Get(baseURL + "/metrics")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
+
+ // authenticated & authorized user should have access to/metrics
+ client.SetBasicAuth(metricsuser, metricspass)
+ resp, err = client.R().Get(baseURL + "/metrics")
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ })
+ })
+}
diff --git a/pkg/api/config/config.go b/pkg/api/config/config.go
index 8ab156e328..c6ed53da26 100644
--- a/pkg/api/config/config.go
+++ b/pkg/api/config/config.go
@@ -131,6 +131,7 @@ type AccessControlConfig struct {
Repositories Repositories `json:"repositories" mapstructure:"repositories"`
AdminPolicy Policy
Groups Groups
+ Metrics Metrics
}
func (config *AccessControlConfig) AnonymousPolicyExists() bool {
@@ -168,6 +169,10 @@ type Policy struct {
Groups []string
}
+type Metrics struct {
+ Users []string
+}
+
type Config struct {
DistSpecVersion string `json:"distSpecVersion" mapstructure:"distSpecVersion"`
GoVersion string
diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go
index 05b53f125a..0566f1685a 100644
--- a/pkg/api/controller_test.go
+++ b/pkg/api/controller_test.go
@@ -7,8 +7,6 @@ import (
"bufio"
"bytes"
"context"
- "crypto/tls"
- "crypto/x509"
"encoding/json"
goerrors "errors"
"fmt"
@@ -19,7 +17,6 @@ import (
"net/url"
"os"
"path"
- "path/filepath"
"sort"
"strconv"
"strings"
@@ -28,8 +25,6 @@ import (
"github.com/google/go-github/v52/github"
"github.com/gorilla/mux"
- "github.com/gorilla/securecookie"
- "github.com/gorilla/sessions"
"github.com/migueleliasweb/go-github-mock/src/mock"
vldap "github.com/nmcclain/ldap"
notreg "github.com/notaryproject/notation-go/registry"
@@ -37,7 +32,6 @@ import (
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1"
- "github.com/project-zot/mockoidc"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/generate"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/sign"
@@ -66,26 +60,24 @@ import (
"zotregistry.io/zot/pkg/test/deprecated"
. "zotregistry.io/zot/pkg/test/image-utils"
"zotregistry.io/zot/pkg/test/inject"
- "zotregistry.io/zot/pkg/test/mocks"
ociutils "zotregistry.io/zot/pkg/test/oci-utils"
"zotregistry.io/zot/pkg/test/signature"
tskip "zotregistry.io/zot/pkg/test/skip"
)
const (
- username = "test"
- htpasswdUsername = "htpasswduser"
- passphrase = "test"
- group = "test"
- repo = "test"
- ServerCert = "../../test/data/server.cert"
- ServerKey = "../../test/data/server.key"
- CACert = "../../test/data/ca.crt"
- AuthorizedNamespace = "everyone/isallowed"
- UnauthorizedNamespace = "fortknox/notallowed"
- ALICE = "alice"
- AuthorizationNamespace = "authz/image"
- AuthorizationAllRepos = "**"
+ username = "test"
+ htpasswdUsername = "htpasswduser"
+ passphrase = "test"
+ group = "test"
+ repo = "test"
+ ServerCert = "../../test/data/server.cert"
+ ServerKey = "../../test/data/server.key"
+ CACert = "../../test/data/ca.crt"
+ AuthorizedNamespace = "everyone/isallowed"
+ UnauthorizedNamespace = "fortknox/notallowed"
+ ALICE = "alice"
+ AuthorizationAllRepos = "**"
)
func getCredString(username, password string) string {
@@ -517,242 +509,6 @@ func TestObjectStorageControllerSubPaths(t *testing.T) {
})
}
-func TestHtpasswdSingleCred(t *testing.T) {
- Convey("Single cred", t, func() {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- singleCredtests := []string{}
- user := ALICE
- password := ALICE
- singleCredtests = append(singleCredtests, getCredString(user, password))
- singleCredtests = append(singleCredtests, getCredString(user, password)+"\n")
-
- for _, testString := range singleCredtests {
- func() {
- conf := config.New()
- conf.HTTP.Port = port
-
- htpasswdPath := test.MakeHtpasswdFileFromString(testString)
- defer os.Remove(htpasswdPath)
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- }
-
- conf.HTTP.AllowOrigin = conf.HTTP.Address
-
- ctlr := makeController(conf, t.TempDir())
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
-
- // with creds, should get expected status code
- resp, _ := resty.R().SetBasicAuth(user, password).Get(baseURL + "/v2/")
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- header := []string{"Authorization,content-type," + constants.SessionClientHeaderName}
-
- resp, _ = resty.R().SetBasicAuth(user, password).Options(baseURL + "/v2/")
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
- So(len(resp.Header()), ShouldEqual, 5)
- So(resp.Header()["Access-Control-Allow-Headers"], ShouldResemble, header)
- So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
-
- // with invalid creds, it should fail
- resp, _ = resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/")
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- }()
- }
- })
-}
-
-func TestAllowMethodsHeader(t *testing.T) {
- Convey("Options request", t, func() {
- dir := t.TempDir()
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- conf := config.New()
- conf.HTTP.Port = port
- conf.Storage.RootDirectory = dir
- conf.HTTP.AllowOrigin = "someOrigin"
-
- simpleUser := "simpleUser"
- simpleUserPassword := "simpleUserPass"
- credTests := fmt.Sprintf("%s\n\n", getCredString(simpleUser, simpleUserPassword))
-
- htpasswdPath := test.MakeHtpasswdFileFromString(credTests)
- defer os.Remove(htpasswdPath)
-
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- }
-
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Repositories: config.Repositories{
- "**": config.PolicyGroup{
- Policies: []config.Policy{
- {
- Users: []string{simpleUser},
- Actions: []string{"read", "create"},
- },
- },
- },
- },
- }
-
- defaultVal := true
- conf.Extensions = &extconf.ExtensionConfig{
- Search: &extconf.SearchConfig{BaseConfig: extconf.BaseConfig{Enable: &defaultVal}},
- }
-
- ctlr := api.NewController(conf)
-
- ctlrManager := test.NewControllerManager(ctlr)
- ctlrManager.StartAndWait(port)
- defer ctlrManager.StopServer()
-
- simpleUserClient := resty.R().SetBasicAuth(simpleUser, simpleUserPassword)
-
- digest := godigest.FromString("digest")
-
- // /v2
- resp, err := simpleUserClient.Options(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
-
- // /v2/{name}/tags/list
- resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/tags/list")
- So(err, ShouldBeNil)
- So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
-
- // /v2/{name}/manifests/{reference}
- resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/manifests/" + digest.String())
- So(err, ShouldBeNil)
- So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "HEAD,GET,OPTIONS")
-
- // /v2/{name}/referrers/{digest}
- resp, err = simpleUserClient.Options(baseURL + "/v2/reponame/referrers/" + digest.String())
- So(err, ShouldBeNil)
- So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
-
- // /v2/_catalog
- resp, err = simpleUserClient.Options(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
-
- // /v2/_oci/ext/discover
- resp, err = simpleUserClient.Options(baseURL + "/v2/_oci/ext/discover")
- So(err, ShouldBeNil)
- So(resp.Header().Get("Access-Control-Allow-Methods"), ShouldResemble, "GET,OPTIONS")
- })
-}
-
-func TestHtpasswdTwoCreds(t *testing.T) {
- Convey("Two creds", t, func() {
- twoCredTests := []string{}
- user1 := "alicia"
- password1 := "aliciapassword"
- user2 := "bob"
- password2 := "robert"
- twoCredTests = append(twoCredTests, getCredString(user1, password1)+"\n"+
- getCredString(user2, password2))
-
- twoCredTests = append(twoCredTests, getCredString(user1, password1)+"\n"+
- getCredString(user2, password2)+"\n")
-
- twoCredTests = append(twoCredTests, getCredString(user1, password1)+"\n\n"+
- getCredString(user2, password2)+"\n\n")
-
- for _, testString := range twoCredTests {
- func() {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- conf := config.New()
- conf.HTTP.Port = port
- htpasswdPath := test.MakeHtpasswdFileFromString(testString)
- defer os.Remove(htpasswdPath)
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- }
- ctlr := makeController(conf, t.TempDir())
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
-
- // with creds, should get expected status code
- resp, _ := resty.R().SetBasicAuth(user1, password1).Get(baseURL + "/v2/")
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- resp, _ = resty.R().SetBasicAuth(user2, password2).Get(baseURL + "/v2/")
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- // with invalid creds, it should fail
- resp, _ = resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/")
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- }()
- }
- })
-}
-
-func TestHtpasswdFiveCreds(t *testing.T) {
- Convey("Five creds", t, func() {
- tests := map[string]string{
- "michael": "scott",
- "jim": "halpert",
- "dwight": "shrute",
- "pam": "bessley",
- "creed": "bratton",
- }
- credString := strings.Builder{}
- for key, val := range tests {
- credString.WriteString(getCredString(key, val) + "\n")
- }
-
- func() {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- conf := config.New()
- conf.HTTP.Port = port
- htpasswdPath := test.MakeHtpasswdFileFromString(credString.String())
- defer os.Remove(htpasswdPath)
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- }
- ctlr := makeController(conf, t.TempDir())
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
-
- // with creds, should get expected status code
- for key, val := range tests {
- resp, _ := resty.R().SetBasicAuth(key, val).Get(baseURL + "/v2/")
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- }
-
- // with invalid creds, it should fail
- resp, _ := resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/")
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- }()
- })
-}
-
func TestRatelimit(t *testing.T) {
Convey("Make a new controller", t, func() {
port := test.GetFreePort()
@@ -1320,47 +1076,124 @@ func TestMultipleInstance(t *testing.T) {
})
}
-func TestTLSWithBasicAuth(t *testing.T) {
+const (
+ LDAPAddress = "127.0.0.1"
+ LDAPBaseDN = "ou=test"
+ LDAPBindDN = "cn=reader," + LDAPBaseDN
+ LDAPBindPassword = "bindPassword"
+)
+
+type testLDAPServer struct {
+ server *vldap.Server
+ quitCh chan bool
+}
+
+func newTestLDAPServer() *testLDAPServer {
+ ldaps := &testLDAPServer{}
+ quitCh := make(chan bool)
+ server := vldap.NewServer()
+ server.QuitChannel(quitCh)
+ server.BindFunc("", ldaps)
+ server.SearchFunc("", ldaps)
+ ldaps.server = server
+ ldaps.quitCh = quitCh
+
+ return ldaps
+}
+
+func (l *testLDAPServer) Start(port int) {
+ addr := fmt.Sprintf("%s:%d", LDAPAddress, port)
+
+ go func() {
+ if err := l.server.ListenAndServe(addr); err != nil {
+ panic(err)
+ }
+ }()
+
+ for {
+ _, err := net.Dial("tcp", addr)
+ if err == nil {
+ break
+ }
+
+ time.Sleep(10 * time.Millisecond)
+ }
+}
+
+func (l *testLDAPServer) Stop() {
+ l.quitCh <- true
+}
+
+func (l *testLDAPServer) Bind(bindDN, bindSimplePw string, conn net.Conn) (vldap.LDAPResultCode, error) {
+ if bindDN == "" || bindSimplePw == "" {
+ return vldap.LDAPResultInappropriateAuthentication, errors.ErrRequireCred
+ }
+
+ if (bindDN == LDAPBindDN && bindSimplePw == LDAPBindPassword) ||
+ (bindDN == fmt.Sprintf("cn=%s,%s", username, LDAPBaseDN) && bindSimplePw == passphrase) {
+ return vldap.LDAPResultSuccess, nil
+ }
+
+ return vldap.LDAPResultInvalidCredentials, errors.ErrInvalidCred
+}
+
+func (l *testLDAPServer) Search(boundDN string, req vldap.SearchRequest,
+ conn net.Conn,
+) (vldap.ServerSearchResult, error) {
+ check := fmt.Sprintf("(uid=%s)", username)
+ if check == req.Filter {
+ return vldap.ServerSearchResult{
+ Entries: []*vldap.Entry{
+ {
+ DN: fmt.Sprintf("cn=%s,%s", username, LDAPBaseDN),
+ Attributes: []*vldap.EntryAttribute{
+ {
+ Name: "memberOf",
+ Values: []string{group},
+ },
+ },
+ },
+ },
+ ResultCode: vldap.LDAPResultSuccess,
+ }, nil
+ }
+
+ return vldap.ServerSearchResult{}, nil
+}
+
+func TestBasicAuthWithLDAP(t *testing.T) {
Convey("Make a new controller", t, func() {
- caCert, err := os.ReadFile(CACert)
+ l := newTestLDAPServer()
+ port := test.GetFreePort()
+ ldapPort, err := strconv.Atoi(port)
So(err, ShouldBeNil)
- caCertPool := x509.NewCertPool()
- caCertPool.AppendCertsFromPEM(caCert)
- htpasswdPath := test.MakeHtpasswdFile()
- defer os.Remove(htpasswdPath)
+ l.Start(ldapPort)
+ defer l.Stop()
- port := test.GetFreePort()
+ port = test.GetFreePort()
baseURL := test.GetBaseURL(port)
- secureBaseURL := test.GetSecureBaseURL(port)
- resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
- defer func() { resty.SetTLSClientConfig(nil) }()
conf := config.New()
conf.HTTP.Port = port
- conf.HTTP.TLS = &config.TLSConfig{
- Cert: ServerCert,
- Key: ServerKey,
- }
conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
+ LDAP: &config.LDAPConfig{
+ Insecure: true,
+ Address: LDAPAddress,
+ Port: ldapPort,
+ BindDN: LDAPBindDN,
+ BindPassword: LDAPBindPassword,
+ BaseDN: LDAPBaseDN,
+ UserAttribute: "uid",
},
}
-
ctlr := makeController(conf, t.TempDir())
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
- // accessing insecure HTTP site should fail
- resp, err := resty.R().Get(baseURL)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
-
// without creds, should get access error
- resp, err = resty.R().Get(secureBaseURL + "/v2/")
+ resp, err := resty.R().Get(baseURL + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
@@ -1369,8447 +1202,4523 @@ func TestTLSWithBasicAuth(t *testing.T) {
So(err, ShouldBeNil)
// with creds, should get expected status code
- resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
+ resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
+ resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + "/v2/")
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // missing password
+ resp, _ = resty.R().SetBasicAuth(username, "").Get(baseURL + "/v2/")
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
})
}
-func TestTLSWithBasicAuthAllowReadAccess(t *testing.T) {
+func TestGroupsPermissionsForLDAP(t *testing.T) {
Convey("Make a new controller", t, func() {
- caCert, err := os.ReadFile(CACert)
+ l := newTestLDAPServer()
+ port := test.GetFreePort()
+ ldapPort, err := strconv.Atoi(port)
So(err, ShouldBeNil)
- caCertPool := x509.NewCertPool()
- caCertPool.AppendCertsFromPEM(caCert)
- htpasswdPath := test.MakeHtpasswdFile()
- defer os.Remove(htpasswdPath)
+ l.Start(ldapPort)
+ defer l.Stop()
- port := test.GetFreePort()
+ port = test.GetFreePort()
baseURL := test.GetBaseURL(port)
- secureBaseURL := test.GetSecureBaseURL(port)
+ tempDir := t.TempDir()
- resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
- defer func() { resty.SetTLSClientConfig(nil) }()
conf := config.New()
conf.HTTP.Port = port
conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
+ LDAP: &config.LDAPConfig{
+ Insecure: true,
+ Address: LDAPAddress,
+ Port: ldapPort,
+ BindDN: LDAPBindDN,
+ BindPassword: LDAPBindPassword,
+ BaseDN: LDAPBaseDN,
+ UserAttribute: "uid",
+ UserGroupAttribute: "memberOf",
},
}
- conf.HTTP.TLS = &config.TLSConfig{
- Cert: ServerCert,
- Key: ServerKey,
- }
conf.HTTP.AccessControl = &config.AccessControlConfig{
- Repositories: config.Repositories{
- AuthorizationAllRepos: config.PolicyGroup{
- AnonymousPolicy: []string{"read"},
+ Groups: config.Groups{
+ group: {
+ Users: []string{username},
},
},
- }
-
- ctlr := makeController(conf, t.TempDir())
-
+ Repositories: config.Repositories{
+ repo: config.PolicyGroup{
+ Policies: []config.Policy{
+ {
+ Groups: []string{group},
+ Actions: []string{"read", "create"},
+ },
+ },
+ DefaultPolicy: []string{},
+ },
+ },
+ AdminPolicy: config.Policy{
+ Users: []string{},
+ Actions: []string{},
+ },
+ }
+
+ ctlr := makeController(conf, tempDir)
+
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
- // accessing insecure HTTP site should fail
- resp, err := resty.R().Get(baseURL)
+ img := CreateDefaultImage()
+
+ err = UploadImageWithBasicAuth(
+ img, baseURL, repo, img.DigestStr(),
+ username, passphrase)
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+ })
+}
- // without creds, should still be allowed to access
- resp, err = resty.R().Get(secureBaseURL + "/v2/")
+func TestLDAPFailures(t *testing.T) {
+ Convey("Make a LDAP conn", t, func() {
+ l := newTestLDAPServer()
+ port := test.GetFreePort()
+ ldapPort, err := strconv.Atoi(port)
So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ l.Start(ldapPort)
+ defer l.Stop()
- // with creds, should get expected status code
- resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ Convey("Empty config", func() {
+ lc := &api.LDAPClient{}
+ err := lc.Connect()
+ So(err, ShouldNotBeNil)
+ })
- resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ Convey("Basic connectivity config", func() {
+ lc := &api.LDAPClient{
+ Host: LDAPAddress,
+ Port: ldapPort,
+ }
+ err := lc.Connect()
+ So(err, ShouldNotBeNil)
+ })
- // without creds, writes should fail
- resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ Convey("Basic TLS connectivity config", func() {
+ lc := &api.LDAPClient{
+ Host: LDAPAddress,
+ Port: ldapPort,
+ UseSSL: true,
+ }
+ err := lc.Connect()
+ So(err, ShouldNotBeNil)
+ })
})
}
-func TestMutualTLSAuthWithUserPermissions(t *testing.T) {
+func TestBearerAuth(t *testing.T) {
Convey("Make a new controller", t, func() {
- caCert, err := os.ReadFile(CACert)
- So(err, ShouldBeNil)
- caCertPool := x509.NewCertPool()
- caCertPool.AppendCertsFromPEM(caCert)
- htpasswdPath := test.MakeHtpasswdFile()
- defer os.Remove(htpasswdPath)
+ authTestServer := authutils.MakeAuthTestServer(ServerKey, UnauthorizedNamespace)
+ defer authTestServer.Close()
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
- secureBaseURL := test.GetSecureBaseURL(port)
- resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
- defer func() { resty.SetTLSClientConfig(nil) }()
conf := config.New()
conf.HTTP.Port = port
- conf.HTTP.TLS = &config.TLSConfig{
- Cert: ServerCert,
- Key: ServerKey,
- CACert: CACert,
- }
+ aurl, err := url.Parse(authTestServer.URL)
+ So(err, ShouldBeNil)
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Repositories: config.Repositories{
- AuthorizationAllRepos: config.PolicyGroup{
- Policies: []config.Policy{
- {
- Users: []string{"*"},
- Actions: []string{"read"},
- },
- },
- },
+ conf.HTTP.Auth = &config.AuthConfig{
+ Bearer: &config.BearerConfig{
+ Cert: ServerCert,
+ Realm: authTestServer.URL + "/auth/token",
+ Service: aurl.Host,
},
}
-
ctlr := makeController(conf, t.TempDir())
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
- resp, err := resty.R().Get(baseURL)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
-
- repoPolicy := conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos]
+ blob := []byte("hello, blob!")
+ digest := godigest.FromBytes(blob).String()
- // setup TLS mutual auth
- cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key")
+ resp, err := resty.R().Get(baseURL + "/v2/")
So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- resty.SetCertificates(cert)
- defer func() { resty.SetCertificates(tls.Certificate{}) }()
-
- // with client certs but without creds, should succeed
- resp, err = resty.R().Get(secureBaseURL + "/v2/")
+ authorizationHeader := authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
+ resp, err = resty.R().
+ SetQueryParam("service", authorizationHeader.Service).
+ SetQueryParam("scope", authorizationHeader.Scope).
+ Get(authorizationHeader.Realm)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ var goodToken authutils.AccessTokenResponse
+ err = json.Unmarshal(resp.Body(), &goodToken)
+ So(err, ShouldBeNil)
- resp, err = resty.R().Get(secureBaseURL + "/v2/_catalog")
+ resp, err = resty.R().
+ SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
+ Get(baseURL + "/v2/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- // with creds, should get expected status code
- resp, _ = resty.R().Get(secureBaseURL)
+ resp, err = resty.R().SetHeader("Authorization",
+ fmt.Sprintf("Bearer %s", goodToken.AccessToken)).Options(baseURL + "/v2/")
+ So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
- // reading a repo should not get 403
- resp, err = resty.R().Get(secureBaseURL + "/v2/repo/tags/list")
+ resp, err = resty.R().Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- // without creds, writes should fail
- resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
+ authorizationHeader = authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
+ resp, err = resty.R().
+ SetQueryParam("service", authorizationHeader.Service).
+ SetQueryParam("scope", authorizationHeader.Scope).
+ Get(authorizationHeader.Realm)
So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
-
- // empty default authorization and give user the permission to create
- repoPolicy.Policies[0].Actions = append(repoPolicy.Policies[0].Actions, "create")
- conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy
- resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ err = json.Unmarshal(resp.Body(), &goodToken)
So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
- })
-}
-func TestMutualTLSAuthWithoutCN(t *testing.T) {
- Convey("Make a new controller", t, func() {
- caCert, err := os.ReadFile("../../test/data/noidentity/ca.crt")
+ resp, err = resty.R().
+ SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
+ Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
So(err, ShouldBeNil)
- caCertPool := x509.NewCertPool()
- caCertPool.AppendCertsFromPEM(caCert)
-
- port := test.GetFreePort()
- secureBaseURL := test.GetSecureBaseURL(port)
-
- resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
- defer func() { resty.SetTLSClientConfig(nil) }()
- conf := config.New()
- conf.HTTP.Port = port
-
- conf.HTTP.TLS = &config.TLSConfig{
- Cert: "../../test/data/noidentity/server.cert",
- Key: "../../test/data/noidentity/server.key",
- CACert: "../../test/data/noidentity/ca.crt",
- }
-
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Repositories: config.Repositories{
- AuthorizationAllRepos: config.PolicyGroup{
- Policies: []config.Policy{
- {
- Users: []string{"*"},
- Actions: []string{"read"},
- },
- },
- },
- },
- }
-
- ctlr := makeController(conf, t.TempDir())
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
+ loc := resp.Header().Get("Location")
- // setup TLS mutual auth
- cert, err := tls.LoadX509KeyPair("../../test/data/noidentity/client.cert", "../../test/data/noidentity/client.key")
+ resp, err = resty.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)
-
- resty.SetCertificates(cert)
- defer func() { resty.SetCertificates(tls.Certificate{}) }()
-
- // with client certs but without TLS mutual auth setup should get certificate error
- resp, _ := resty.R().Get(secureBaseURL + "/v2/_catalog")
+ So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- })
-}
-func TestTLSMutualAuth(t *testing.T) {
- Convey("Make a new controller", t, func() {
- caCert, err := os.ReadFile(CACert)
+ authorizationHeader = authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
+ resp, err = resty.R().
+ SetQueryParam("service", authorizationHeader.Service).
+ SetQueryParam("scope", authorizationHeader.Scope).
+ Get(authorizationHeader.Realm)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ err = json.Unmarshal(resp.Body(), &goodToken)
So(err, ShouldBeNil)
- caCertPool := x509.NewCertPool()
- caCertPool.AppendCertsFromPEM(caCert)
-
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- secureBaseURL := test.GetSecureBaseURL(port)
-
- resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
- defer func() { resty.SetTLSClientConfig(nil) }()
- conf := config.New()
- conf.HTTP.Port = port
- conf.HTTP.TLS = &config.TLSConfig{
- Cert: ServerCert,
- Key: ServerKey,
- CACert: CACert,
- }
-
- ctlr := makeController(conf, t.TempDir())
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
- // accessing insecure HTTP site should fail
- resp, err := resty.R().Get(baseURL)
+ resp, err = resty.R().
+ SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))).
+ SetHeader("Content-Type", "application/octet-stream").
+ SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
+ SetQueryParam("digest", digest).
+ SetBody(blob).
+ Put(baseURL + loc)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
-
- // without client certs and creds, should get conn error
- _, err = resty.R().Get(secureBaseURL)
- So(err, ShouldNotBeNil)
-
- // with creds but without certs, should get conn error
- _, err = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
- So(err, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- // setup TLS mutual auth
- cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key")
+ resp, err = resty.R().
+ SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
+ Get(baseURL + "/v2/" + AuthorizedNamespace + "/tags/list")
So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- resty.SetCertificates(cert)
- defer func() { resty.SetCertificates(tls.Certificate{}) }()
+ authorizationHeader = authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
+ resp, err = resty.R().
+ SetQueryParam("service", authorizationHeader.Service).
+ SetQueryParam("scope", authorizationHeader.Scope).
+ Get(authorizationHeader.Realm)
+ So(err, ShouldBeNil)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ err = json.Unmarshal(resp.Body(), &goodToken)
+ So(err, ShouldBeNil)
- // with client certs but without creds, should succeed
- resp, err = resty.R().Get(secureBaseURL + "/v2/")
+ resp, err = resty.R().
+ SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
+ Get(baseURL + "/v2/" + AuthorizedNamespace + "/tags/list")
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- // with client certs and creds, should get expected status code
- resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
+ resp, err = resty.R().
+ Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/")
+ So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- // with client certs, creds shouldn't matter
- resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
+ authorizationHeader = authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
+ resp, err = resty.R().
+ SetQueryParam("service", authorizationHeader.Service).
+ SetQueryParam("scope", authorizationHeader.Scope).
+ Get(authorizationHeader.Realm)
+ So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- })
-}
+ var badToken authutils.AccessTokenResponse
+ err = json.Unmarshal(resp.Body(), &badToken)
+ So(err, ShouldBeNil)
-func TestTLSMutualAuthAllowReadAccess(t *testing.T) {
- Convey("Make a new controller", t, func() {
- caCert, err := os.ReadFile(CACert)
+ resp, err = resty.R().
+ SetHeader("Authorization", fmt.Sprintf("Bearer %s", badToken.AccessToken)).
+ Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/")
So(err, ShouldBeNil)
- caCertPool := x509.NewCertPool()
- caCertPool.AppendCertsFromPEM(caCert)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ })
+}
+func TestBearerAuthWrongAuthorizer(t *testing.T) {
+ Convey("Make a new authorizer", t, func() {
port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- secureBaseURL := test.GetSecureBaseURL(port)
- resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
- defer func() { resty.SetTLSClientConfig(nil) }()
conf := config.New()
conf.HTTP.Port = port
- conf.HTTP.TLS = &config.TLSConfig{
- Cert: ServerCert,
- Key: ServerKey,
- CACert: CACert,
- }
-
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Repositories: config.Repositories{
- AuthorizationAllRepos: config.PolicyGroup{
- AnonymousPolicy: []string{"read"},
- },
+ conf.HTTP.Auth = &config.AuthConfig{
+ Bearer: &config.BearerConfig{
+ Cert: "bla",
+ Realm: "blabla",
+ Service: "blablabla",
},
}
-
ctlr := makeController(conf, t.TempDir())
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ So(func() {
+ api.AuthHandler(ctlr)
+ }, ShouldPanic)
+ })
+}
- // accessing insecure HTTP site should fail
- resp, err := resty.R().Get(baseURL)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+func TestNewRelyingPartyOIDC(t *testing.T) {
+ Convey("Test NewRelyingPartyOIDC", t, func() {
+ conf := config.New()
- // without client certs and creds, reads are allowed
- resp, err = resty.R().Get(secureBaseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ mockOIDCServer, err := authutils.MockOIDCRun()
+ if err != nil {
+ panic(err)
+ }
- // with creds but without certs, reads are allowed
- resp, err = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ defer func() {
+ err := mockOIDCServer.Shutdown()
+ if err != nil {
+ panic(err)
+ }
+ }()
- // without creds, writes should fail
- resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ mockOIDCConfig := mockOIDCServer.Config()
- // setup TLS mutual auth
- cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key")
- So(err, ShouldBeNil)
+ conf.HTTP.Auth = &config.AuthConfig{
+ OpenID: &config.OpenIDConfig{
+ Providers: map[string]config.OpenIDProviderConfig{
+ "oidc": {
+ ClientID: mockOIDCConfig.ClientID,
+ ClientSecret: mockOIDCConfig.ClientSecret,
+ KeyPath: "",
+ Issuer: mockOIDCConfig.Issuer,
+ Scopes: []string{"openid", "email"},
+ },
+ },
+ },
+ }
- resty.SetCertificates(cert)
- defer func() { resty.SetCertificates(tls.Certificate{}) }()
+ Convey("provider not found in config", func() {
+ So(func() { _ = api.NewRelyingPartyOIDC(conf, "notDex") }, ShouldPanic)
+ })
- // with client certs but without creds, should succeed
- resp, err = resty.R().Get(secureBaseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ Convey("key path not found on disk", func() {
+ oidcProviderCfg := conf.HTTP.Auth.OpenID.Providers["oidc"]
+ oidcProviderCfg.KeyPath = "path/to/file"
+ conf.HTTP.Auth.OpenID.Providers["oidc"] = oidcProviderCfg
- // with client certs and creds, should get expected status code
- resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ So(func() { _ = api.NewRelyingPartyOIDC(conf, "oidc") }, ShouldPanic)
+ })
- // with client certs, creds shouldn't matter
- resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ Convey("https callback", func() {
+ conf.HTTP.TLS = &config.TLSConfig{
+ Cert: ServerCert,
+ Key: ServerKey,
+ }
+
+ rp := api.NewRelyingPartyOIDC(conf, "oidc")
+ So(rp, ShouldNotBeNil)
+ })
+
+ Convey("no client secret in config", func() {
+ oidcProvider := conf.HTTP.Auth.OpenID.Providers["oidc"]
+ oidcProvider.ClientSecret = ""
+ conf.HTTP.Auth.OpenID.Providers["oidc"] = oidcProvider
+
+ rp := api.NewRelyingPartyOIDC(conf, "oidc")
+ So(rp, ShouldNotBeNil)
+ })
+
+ Convey("provider issuer unreachable", func() {
+ oidcProvider := conf.HTTP.Auth.OpenID.Providers["oidc"]
+ oidcProvider.Issuer = ""
+ conf.HTTP.Auth.OpenID.Providers["oidc"] = oidcProvider
+
+ So(func() { _ = api.NewRelyingPartyOIDC(conf, "oidc") }, ShouldPanic)
+ })
})
}
-func TestTLSMutualAndBasicAuth(t *testing.T) {
- Convey("Make a new controller", t, func() {
- caCert, err := os.ReadFile(CACert)
- So(err, ShouldBeNil)
- caCertPool := x509.NewCertPool()
- caCertPool.AppendCertsFromPEM(caCert)
- htpasswdPath := test.MakeHtpasswdFile()
- defer os.Remove(htpasswdPath)
-
+func TestInvalidCases(t *testing.T) {
+ Convey("Invalid repo dir", t, func() {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
- secureBaseURL := test.GetSecureBaseURL(port)
- resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
- defer func() { resty.SetTLSClientConfig(nil) }()
conf := config.New()
conf.HTTP.Port = port
+ htpasswdPath := test.MakeHtpasswdFileFromString(getCredString(username, passphrase))
+
+ defer os.Remove(htpasswdPath)
+
conf.HTTP.Auth = &config.AuthConfig{
HTPasswd: config.AuthHTPasswd{
Path: htpasswdPath,
},
}
- conf.HTTP.TLS = &config.TLSConfig{
- Cert: ServerCert,
- Key: ServerKey,
- CACert: CACert,
- }
- ctlr := makeController(conf, t.TempDir())
+ dir := t.TempDir()
+ ctlr := makeController(conf, dir)
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(port)
- defer cm.StopServer()
-
- // accessing insecure HTTP site should fail
- resp, err := resty.R().Get(baseURL)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+ defer func(ctrl *api.Controller) {
+ err := os.Chmod(dir, 0o755)
+ if err != nil {
+ panic(err)
+ }
- // without client certs and creds, should fail
- _, err = resty.R().Get(secureBaseURL)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+ err = ctrl.Server.Shutdown(context.Background())
+ if err != nil {
+ panic(err)
+ }
- // with creds but without certs, should succeed
- _, err = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+ err = os.RemoveAll(ctrl.Config.Storage.RootDirectory)
+ if err != nil {
+ panic(err)
+ }
+ }(ctlr)
- // setup TLS mutual auth
- cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key")
- So(err, ShouldBeNil)
+ err := os.Chmod(dir, 0o000)
+ if err != nil {
+ panic(err)
+ }
- resty.SetCertificates(cert)
- defer func() { resty.SetCertificates(tls.Certificate{}) }()
+ digest := godigest.FromString("dummy").String()
+ name := "zot-c-test"
- // with client certs but without creds, should get access error
- resp, err = resty.R().Get(secureBaseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ client := resty.New()
- // with client certs and creds, should get expected status code
- resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ params := make(map[string]string)
+ params["from"] = "zot-cveid-test"
+ params["mount"] = digest
- resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ postResponse, err := client.R().
+ SetBasicAuth(username, passphrase).SetQueryParams(params).
+ Post(fmt.Sprintf("%s/v2/%s/blobs/uploads/", baseURL, name))
+ So(err, ShouldBeNil)
+ So(postResponse.StatusCode(), ShouldEqual, http.StatusInternalServerError)
})
}
-func TestTLSMutualAndBasicAuthAllowReadAccess(t *testing.T) {
- Convey("Make a new controller", t, func() {
- caCert, err := os.ReadFile(CACert)
- So(err, ShouldBeNil)
- caCertPool := x509.NewCertPool()
- caCertPool.AppendCertsFromPEM(caCert)
- htpasswdPath := test.MakeHtpasswdFile()
- defer os.Remove(htpasswdPath)
-
+func TestCrossRepoMount(t *testing.T) {
+ Convey("Cross Repo Mount", t, func() {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
- secureBaseURL := test.GetSecureBaseURL(port)
- resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool, MinVersion: tls.VersionTLS12})
- defer func() { resty.SetTLSClientConfig(nil) }()
conf := config.New()
conf.HTTP.Port = port
+ htpasswdPath := test.MakeHtpasswdFileFromString(getCredString(username, passphrase))
+
+ defer os.Remove(htpasswdPath)
+
conf.HTTP.Auth = &config.AuthConfig{
HTPasswd: config.AuthHTPasswd{
Path: htpasswdPath,
},
}
- conf.HTTP.TLS = &config.TLSConfig{
- Cert: ServerCert,
- Key: ServerKey,
- CACert: CACert,
- }
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Repositories: config.Repositories{
- AuthorizationAllRepos: config.PolicyGroup{
- AnonymousPolicy: []string{"read"},
- },
- },
- }
+ dir := t.TempDir()
+ ctlr := api.NewController(conf)
- ctlr := makeController(conf, t.TempDir())
+ ctlr.Config.Storage.RootDirectory = dir
+ ctlr.Config.Storage.RemoteCache = false
+ ctlr.Config.Storage.Dedupe = false
- cm := test.NewControllerManager(ctlr)
+ image := CreateDefaultImage()
+ err := WriteImageToFileSystem(image, "zot-cve-test", "test", storage.StoreController{
+ DefaultStore: ociutils.GetDefaultImageStore(dir, ctlr.Log),
+ })
+ So(err, ShouldBeNil)
+
+ cm := test.NewControllerManager(ctlr) //nolint: varnamelen
cm.StartAndWait(port)
- defer cm.StopServer()
- // accessing insecure HTTP site should fail
- resp, err := resty.R().Get(baseURL)
+ params := make(map[string]string)
+
+ manifestDigest := image.ManifestDescriptor.Digest
+
+ dgst := manifestDigest
+ name := "zot-cve-test"
+ params["mount"] = string(manifestDigest)
+ params["from"] = name
+
+ client := resty.New()
+ headResponse, err := client.R().SetBasicAuth(username, passphrase).
+ Head(fmt.Sprintf("%s/v2/%s/blobs/%s", baseURL, name, manifestDigest))
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+ So(headResponse.StatusCode(), ShouldEqual, http.StatusOK)
+
+ // All invalid request of mount should return 202.
+ params["mount"] = "sha:"
- // without client certs and creds, should fail
- _, err = resty.R().Get(secureBaseURL)
+ postResponse, err := client.R().
+ SetBasicAuth(username, passphrase).SetQueryParams(params).
+ Post(baseURL + "/v2/zot-c-test/blobs/uploads/")
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+ So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
+ location, err := postResponse.RawResponse.Location()
+ So(err, ShouldBeNil)
+ So(location.String(), ShouldStartWith, fmt.Sprintf("%s%s/zot-c-test/%s/%s",
+ baseURL, constants.RoutePrefix, constants.Blobs, constants.Uploads))
+
+ incorrectParams := make(map[string]string)
+ incorrectParams["mount"] = godigest.FromString("dummy").String()
+ incorrectParams["from"] = "zot-x-test"
- // with creds but without certs, should succeed
- _, err = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
+ postResponse, err = client.R().
+ SetBasicAuth(username, passphrase).SetQueryParams(incorrectParams).
+ Post(baseURL + "/v2/zot-y-test/blobs/uploads/")
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+ So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
+ So(test.Location(baseURL, postResponse), ShouldStartWith, fmt.Sprintf("%s%s/zot-y-test/%s/%s",
+ baseURL, constants.RoutePrefix, constants.Blobs, constants.Uploads))
- // setup TLS mutual auth
- cert, err := tls.LoadX509KeyPair("../../test/data/client.cert", "../../test/data/client.key")
+ // Use correct request
+ // This is correct request but it will return 202 because blob is not present in cache.
+ params["mount"] = string(manifestDigest)
+ postResponse, err = client.R().
+ SetBasicAuth(username, passphrase).SetQueryParams(params).
+ Post(baseURL + "/v2/zot-c-test/blobs/uploads/")
So(err, ShouldBeNil)
+ So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
+ So(test.Location(baseURL, postResponse), ShouldStartWith, fmt.Sprintf("%s%s/zot-c-test/%s/%s",
+ baseURL, constants.RoutePrefix, constants.Blobs, constants.Uploads))
- resty.SetCertificates(cert)
- defer func() { resty.SetCertificates(tls.Certificate{}) }()
+ // Send same request again
+ postResponse, err = client.R().
+ SetBasicAuth(username, passphrase).SetQueryParams(params).
+ Post(baseURL + "/v2/zot-c-test/blobs/uploads/")
+ So(err, ShouldBeNil)
+ So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
- // with client certs but without creds, reads should succeed
- resp, err = resty.R().Get(secureBaseURL + "/v2/")
+ // Valid requests
+ postResponse, err = client.R().
+ SetBasicAuth(username, passphrase).SetQueryParams(params).
+ Post(baseURL + "/v2/zot-d-test/blobs/uploads/")
So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
- // with only client certs, writes should fail
- resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/")
+ headResponse, err = client.R().SetBasicAuth(username, passphrase).
+ Head(fmt.Sprintf("%s/v2/zot-cv-test/blobs/%s", baseURL, manifestDigest))
So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ So(headResponse.StatusCode(), ShouldEqual, http.StatusNotFound)
- // with client certs and creds, should get expected status code
- resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ postResponse, err = client.R().
+ SetBasicAuth(username, passphrase).SetQueryParams(params).Post(baseURL + "/v2/zot-c-test/blobs/uploads/")
+ So(err, ShouldBeNil)
+ So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
- resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/")
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- })
-}
-
-const (
- LDAPAddress = "127.0.0.1"
- LDAPBaseDN = "ou=test"
- LDAPBindDN = "cn=reader," + LDAPBaseDN
- LDAPBindPassword = "bindPassword"
-)
-
-type testLDAPServer struct {
- server *vldap.Server
- quitCh chan bool
-}
-
-func newTestLDAPServer() *testLDAPServer {
- ldaps := &testLDAPServer{}
- quitCh := make(chan bool)
- server := vldap.NewServer()
- server.QuitChannel(quitCh)
- server.BindFunc("", ldaps)
- server.SearchFunc("", ldaps)
- ldaps.server = server
- ldaps.quitCh = quitCh
-
- return ldaps
-}
+ postResponse, err = client.R().
+ SetBasicAuth(username, passphrase).SetQueryParams(params).
+ Post(baseURL + "/v2/ /blobs/uploads/")
+ So(err, ShouldBeNil)
+ So(postResponse.StatusCode(), ShouldEqual, http.StatusNotFound)
-func (l *testLDAPServer) Start(port int) {
- addr := fmt.Sprintf("%s:%d", LDAPAddress, port)
+ blob := manifestDigest.Encoded()
- go func() {
- if err := l.server.ListenAndServe(addr); err != nil {
+ buf, err := os.ReadFile(path.Join(ctlr.Config.Storage.RootDirectory, "zot-cve-test/blobs/sha256/"+blob))
+ if err != nil {
panic(err)
}
- }()
- for {
- _, err := net.Dial("tcp", addr)
- if err == nil {
- break
- }
+ postResponse, err = client.R().SetHeader("Content-type", "application/octet-stream").
+ SetBasicAuth(username, passphrase).SetQueryParam("digest", "sha256:"+blob).
+ SetBody(buf).Post(baseURL + "/v2/zot-d-test/blobs/uploads/")
+ So(err, ShouldBeNil)
+ So(postResponse.StatusCode(), ShouldEqual, http.StatusCreated)
- time.Sleep(10 * time.Millisecond)
- }
-}
+ // We have uploaded a blob and since we have provided digest it should be full blob upload and there should be entry
+ // in cache, now try mount blob request status and it should be 201 because now blob is present in cache
+ // and it should do hard link.
-func (l *testLDAPServer) Stop() {
- l.quitCh <- true
-}
+ // make a new server with dedupe on and same rootDir (can't restart because of metadb - boltdb being open)
+ newDir := t.TempDir()
+ err = test.CopyFiles(dir, newDir)
+ So(err, ShouldBeNil)
-func (l *testLDAPServer) Bind(bindDN, bindSimplePw string, conn net.Conn) (vldap.LDAPResultCode, error) {
- if bindDN == "" || bindSimplePw == "" {
- return vldap.LDAPResultInappropriateAuthentication, errors.ErrRequireCred
- }
+ cm.StopServer()
- if (bindDN == LDAPBindDN && bindSimplePw == LDAPBindPassword) ||
- (bindDN == fmt.Sprintf("cn=%s,%s", username, LDAPBaseDN) && bindSimplePw == passphrase) {
- return vldap.LDAPResultSuccess, nil
- }
+ ctlr.Config.Storage.Dedupe = true
+ ctlr.Config.Storage.GC = false
+ ctlr.Config.Storage.RootDirectory = newDir
+ cm = test.NewControllerManager(ctlr) //nolint: varnamelen
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- return vldap.LDAPResultInvalidCredentials, errors.ErrInvalidCred
-}
+ // wait for dedupe task to run
+ time.Sleep(10 * time.Second)
-func (l *testLDAPServer) Search(boundDN string, req vldap.SearchRequest,
- conn net.Conn,
-) (vldap.ServerSearchResult, error) {
- check := fmt.Sprintf("(uid=%s)", username)
- if check == req.Filter {
- return vldap.ServerSearchResult{
- Entries: []*vldap.Entry{
- {
- DN: fmt.Sprintf("cn=%s,%s", username, LDAPBaseDN),
- Attributes: []*vldap.EntryAttribute{
- {
- Name: "memberOf",
- Values: []string{group},
- },
- },
- },
- },
- ResultCode: vldap.LDAPResultSuccess,
- }, nil
- }
+ params["mount"] = string(manifestDigest)
+ postResponse, err = client.R().
+ SetBasicAuth(username, passphrase).SetQueryParams(params).
+ Post(baseURL + "/v2/zot-mount-test/blobs/uploads/")
+ So(err, ShouldBeNil)
+ So(postResponse.StatusCode(), ShouldEqual, http.StatusCreated)
+ So(test.Location(baseURL, postResponse), ShouldEqual, fmt.Sprintf("%s%s/zot-mount-test/%s/%s:%s",
+ baseURL, constants.RoutePrefix, constants.Blobs, godigest.SHA256, blob))
- return vldap.ServerSearchResult{}, nil
-}
+ // Check os.SameFile here
+ cachePath := path.Join(ctlr.Config.Storage.RootDirectory, "zot-d-test", "blobs/sha256", dgst.Encoded())
-func TestBasicAuthWithLDAP(t *testing.T) {
- Convey("Make a new controller", t, func() {
- l := newTestLDAPServer()
- port := test.GetFreePort()
- ldapPort, err := strconv.Atoi(port)
+ cacheFi, err := os.Stat(cachePath)
So(err, ShouldBeNil)
- l.Start(ldapPort)
- defer l.Stop()
- port = test.GetFreePort()
- baseURL := test.GetBaseURL(port)
+ linkPath := path.Join(ctlr.Config.Storage.RootDirectory, "zot-mount-test", "blobs/sha256", dgst.Encoded())
- conf := config.New()
- conf.HTTP.Port = port
- conf.HTTP.Auth = &config.AuthConfig{
- LDAP: &config.LDAPConfig{
- Insecure: true,
- Address: LDAPAddress,
- Port: ldapPort,
- BindDN: LDAPBindDN,
- BindPassword: LDAPBindPassword,
- BaseDN: LDAPBaseDN,
- UserAttribute: "uid",
- },
- }
- ctlr := makeController(conf, t.TempDir())
+ linkFi, err := os.Stat(linkPath)
+ So(err, ShouldBeNil)
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ So(os.SameFile(cacheFi, linkFi), ShouldEqual, true)
- // without creds, should get access error
- resp, err := resty.R().Get(baseURL + "/v2/")
+ // Now try another mount request and this time it should be from above uploaded repo i.e zot-mount-test
+ // mount request should pass and should return 201.
+ params["mount"] = string(manifestDigest)
+ params["from"] = "zot-mount-test"
+ postResponse, err = client.R().
+ SetBasicAuth(username, passphrase).SetQueryParams(params).
+ Post(baseURL + "/v2/zot-mount1-test/blobs/uploads/")
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- var e apiErr.Error
- err = json.Unmarshal(resp.Body(), &e)
+ So(postResponse.StatusCode(), ShouldEqual, http.StatusCreated)
+ So(test.Location(baseURL, postResponse), ShouldEqual, fmt.Sprintf("%s%s/zot-mount1-test/%s/%s:%s",
+ baseURL, constants.RoutePrefix, constants.Blobs, godigest.SHA256, blob))
+
+ linkPath = path.Join(ctlr.Config.Storage.RootDirectory, "zot-mount1-test", "blobs/sha256", dgst.Encoded())
+
+ linkFi, err = os.Stat(linkPath)
So(err, ShouldBeNil)
- // with creds, should get expected status code
- resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ So(os.SameFile(cacheFi, linkFi), ShouldEqual, true)
- resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + "/v2/")
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ headResponse, err = client.R().SetBasicAuth(username, passphrase).
+ Head(fmt.Sprintf("%s/v2/zot-cv-test/blobs/%s", baseURL, manifestDigest))
+ So(err, ShouldBeNil)
+ So(headResponse.StatusCode(), ShouldEqual, http.StatusOK)
- // missing password
- resp, _ = resty.R().SetBasicAuth(username, "").Get(baseURL + "/v2/")
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- })
-}
+ // Invalid request
+ params = make(map[string]string)
+ params["mount"] = "sha256:"
+ postResponse, err = client.R().
+ SetBasicAuth(username, passphrase).SetQueryParams(params).
+ Post(baseURL + "/v2/zot-mount-test/blobs/uploads/")
+ So(err, ShouldBeNil)
+ So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
-func TestGroupsPermissionsForLDAP(t *testing.T) {
- Convey("Make a new controller", t, func() {
- l := newTestLDAPServer()
- port := test.GetFreePort()
- ldapPort, err := strconv.Atoi(port)
+ params = make(map[string]string)
+ params["from"] = "zot-cve-test"
+ postResponse, err = client.R().
+ SetBasicAuth(username, passphrase).SetQueryParams(params).
+ Post(baseURL + "/v2/zot-mount-test/blobs/uploads/")
So(err, ShouldBeNil)
- l.Start(ldapPort)
- defer l.Stop()
+ So(postResponse.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
+ })
- port = test.GetFreePort()
+ Convey("Disable dedupe and cache", t, func() {
+ port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
- tempDir := t.TempDir()
conf := config.New()
conf.HTTP.Port = port
+ htpasswdPath := test.MakeHtpasswdFileFromString(getCredString(username, passphrase))
+
+ defer os.Remove(htpasswdPath)
+
conf.HTTP.Auth = &config.AuthConfig{
- LDAP: &config.LDAPConfig{
- Insecure: true,
- Address: LDAPAddress,
- Port: ldapPort,
- BindDN: LDAPBindDN,
- BindPassword: LDAPBindPassword,
- BaseDN: LDAPBaseDN,
- UserAttribute: "uid",
- UserGroupAttribute: "memberOf",
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
},
}
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Groups: config.Groups{
- group: {
- Users: []string{username},
- },
- },
- Repositories: config.Repositories{
- repo: config.PolicyGroup{
- Policies: []config.Policy{
- {
- Groups: []string{group},
- Actions: []string{"read", "create"},
- },
- },
- DefaultPolicy: []string{},
- },
- },
- AdminPolicy: config.Policy{
- Users: []string{},
- Actions: []string{},
- },
- }
+ dir := t.TempDir()
- ctlr := makeController(conf, tempDir)
+ ctlr := api.NewController(conf)
+ ctlr.Config.Storage.RootDirectory = dir
+ ctlr.Config.Storage.Dedupe = false
+ ctlr.Config.Storage.GC = false
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ image := CreateImageWith().RandomLayers(1, 10).DefaultConfig().Build()
- img := CreateDefaultImage()
+ err := WriteImageToFileSystem(image, "zot-cve-test", "0.0.1",
+ ociutils.GetDefaultStoreController(dir, ctlr.Log))
+ So(err, ShouldBeNil)
- err = UploadImageWithBasicAuth(
- img, baseURL, repo, img.DigestStr(),
- username, passphrase)
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ // digest := test.GetTestBlobDigest("zot-cve-test", "layer").String()
+ digest := godigest.FromBytes(image.Layers[0])
+ name := "zot-c-test"
+ client := resty.New()
+ headResponse, err := client.R().SetBasicAuth(username, passphrase).
+ Head(fmt.Sprintf("%s/v2/%s/blobs/%s", baseURL, name, digest))
So(err, ShouldBeNil)
+ So(headResponse.StatusCode(), ShouldEqual, http.StatusNotFound)
})
}
-func TestLDAPFailures(t *testing.T) {
- Convey("Make a LDAP conn", t, func() {
- l := newTestLDAPServer()
- port := test.GetFreePort()
- ldapPort, err := strconv.Atoi(port)
- So(err, ShouldBeNil)
- l.Start(ldapPort)
- defer l.Stop()
-
- Convey("Empty config", func() {
- lc := &api.LDAPClient{}
- err := lc.Connect()
- So(err, ShouldNotBeNil)
- })
+func TestParallelRequests(t *testing.T) {
+ t.Parallel()
- Convey("Basic connectivity config", func() {
- lc := &api.LDAPClient{
- Host: LDAPAddress,
- Port: ldapPort,
- }
- err := lc.Connect()
- So(err, ShouldNotBeNil)
- })
+ testCases := []struct {
+ srcImageName string
+ srcImageTag string
+ destImageName string
+ destImageTag string
+ testCaseName string
+ }{
+ {
+ srcImageName: "zot-test",
+ srcImageTag: "0.0.1",
+ destImageName: "zot-1-test",
+ destImageTag: "0.0.1",
+ testCaseName: "Request-1",
+ },
+ {
+ srcImageName: "zot-test",
+ srcImageTag: "0.0.1",
+ destImageName: "zot-2-test",
+ testCaseName: "Request-2",
+ },
+ {
+ srcImageName: "zot-cve-test",
+ srcImageTag: "0.0.1",
+ destImageName: "a/zot-3-test",
+ testCaseName: "Request-3",
+ },
+ {
+ srcImageName: "zot-cve-test",
+ srcImageTag: "0.0.1",
+ destImageName: "b/zot-4-test",
+ testCaseName: "Request-4",
+ },
+ {
+ srcImageName: "zot-cve-test",
+ srcImageTag: "0.0.1",
+ destImageName: "zot-5-test",
+ testCaseName: "Request-5",
+ },
+ {
+ srcImageName: "zot-cve-test",
+ srcImageTag: "0.0.1",
+ destImageName: "zot-1-test",
+ testCaseName: "Request-6",
+ },
+ {
+ srcImageName: "zot-cve-test",
+ srcImageTag: "0.0.1",
+ destImageName: "zot-2-test",
+ testCaseName: "Request-7",
+ },
+ {
+ srcImageName: "zot-cve-test",
+ srcImageTag: "0.0.1",
+ destImageName: "zot-3-test",
+ testCaseName: "Request-8",
+ },
+ {
+ srcImageName: "zot-cve-test",
+ srcImageTag: "0.0.1",
+ destImageName: "zot-4-test",
+ testCaseName: "Request-9",
+ },
+ {
+ srcImageName: "zot-cve-test",
+ srcImageTag: "0.0.1",
+ destImageName: "zot-5-test",
+ testCaseName: "Request-10",
+ },
+ {
+ srcImageName: "zot-test",
+ srcImageTag: "0.0.1",
+ destImageName: "zot-1-test",
+ destImageTag: "0.0.1",
+ testCaseName: "Request-11",
+ },
+ {
+ srcImageName: "zot-test",
+ srcImageTag: "0.0.1",
+ destImageName: "zot-2-test",
+ testCaseName: "Request-12",
+ },
+ {
+ srcImageName: "zot-cve-test",
+ srcImageTag: "0.0.1",
+ destImageName: "a/zot-3-test",
+ testCaseName: "Request-13",
+ },
+ {
+ srcImageName: "zot-cve-test",
+ srcImageTag: "0.0.1",
+ destImageName: "b/zot-4-test",
+ testCaseName: "Request-14",
+ },
+ }
- Convey("Basic TLS connectivity config", func() {
- lc := &api.LDAPClient{
- Host: LDAPAddress,
- Port: ldapPort,
- UseSSL: true,
- }
- err := lc.Connect()
- So(err, ShouldNotBeNil)
- })
- })
-}
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
-func TestBearerAuth(t *testing.T) {
- Convey("Make a new controller", t, func() {
- authTestServer := authutils.MakeAuthTestServer(ServerKey, UnauthorizedNamespace)
- defer authTestServer.Close()
+ conf := config.New()
+ conf.HTTP.Port = port
+ htpasswdPath := test.MakeHtpasswdFileFromString(getCredString(username, passphrase))
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
+ conf.HTTP.Auth = &config.AuthConfig{
+ HTPasswd: config.AuthHTPasswd{
+ Path: htpasswdPath,
+ },
+ }
- conf := config.New()
- conf.HTTP.Port = port
+ dir := t.TempDir()
+ firstSubDir := t.TempDir()
+ secondSubDir := t.TempDir()
- aurl, err := url.Parse(authTestServer.URL)
- So(err, ShouldBeNil)
+ subPaths := make(map[string]config.StorageConfig)
- conf.HTTP.Auth = &config.AuthConfig{
- Bearer: &config.BearerConfig{
- Cert: ServerCert,
- Realm: authTestServer.URL + "/auth/token",
- Service: aurl.Host,
- },
- }
- ctlr := makeController(conf, t.TempDir())
+ subPaths["/a"] = config.StorageConfig{RootDirectory: firstSubDir}
+ subPaths["/b"] = config.StorageConfig{RootDirectory: secondSubDir}
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ ctlr := makeController(conf, dir)
+ ctlr.Config.Storage.SubPaths = subPaths
- blob := []byte("hello, blob!")
- digest := godigest.FromBytes(blob).String()
+ testImagesDir := t.TempDir()
+ testImagesController := ociutils.GetDefaultStoreController(testImagesDir, ctlr.Log)
- resp, err := resty.R().Get(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ err := WriteImageToFileSystem(CreateRandomImage(), "zot-test", "0.0.1", testImagesController)
+ assert.Equal(t, err, nil, "Error should be nil")
- authorizationHeader := authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
- resp, err = resty.R().
- SetQueryParam("service", authorizationHeader.Service).
- SetQueryParam("scope", authorizationHeader.Scope).
- Get(authorizationHeader.Realm)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- var goodToken authutils.AccessTokenResponse
- err = json.Unmarshal(resp.Body(), &goodToken)
- So(err, ShouldBeNil)
+ err = WriteImageToFileSystem(CreateRandomImage(), "zot-cve-test", "0.0.1", testImagesController)
+ assert.Equal(t, err, nil, "Error should be nil")
- resp, err = resty.R().
- SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
- Get(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
- resp, err = resty.R().SetHeader("Authorization",
- fmt.Sprintf("Bearer %s", goodToken.AccessToken)).Options(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
+ // without creds, should get access error
+ for i, testcase := range testCases {
+ testcase := testcase
+ run := i
- resp, err = resty.R().Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ t.Run(testcase.testCaseName, func(t *testing.T) {
+ t.Parallel()
+ client := resty.New()
- authorizationHeader = authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
- resp, err = resty.R().
- SetQueryParam("service", authorizationHeader.Service).
- SetQueryParam("scope", authorizationHeader.Scope).
- Get(authorizationHeader.Realm)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- err = json.Unmarshal(resp.Body(), &goodToken)
- So(err, ShouldBeNil)
+ tagResponse, err := client.R().SetBasicAuth(username, passphrase).
+ Get(baseURL + "/v2/" + testcase.destImageName + "/tags/list")
+ assert.Equal(t, err, nil, "Error should be nil")
+ assert.NotEqual(t, tagResponse.StatusCode(), http.StatusBadRequest, "bad request")
- resp, err = resty.R().
- SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
- Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
- loc := resp.Header().Get("Location")
+ manifestList := getAllManifests(path.Join(testImagesDir, testcase.srcImageName))
- resp, err = resty.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.StatusUnauthorized)
-
- authorizationHeader = authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
- resp, err = resty.R().
- SetQueryParam("service", authorizationHeader.Service).
- SetQueryParam("scope", authorizationHeader.Scope).
- Get(authorizationHeader.Realm)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- err = json.Unmarshal(resp.Body(), &goodToken)
- So(err, ShouldBeNil)
+ for _, manifest := range manifestList {
+ headResponse, err := client.R().SetBasicAuth(username, passphrase).
+ Head(baseURL + "/v2/" + testcase.destImageName + "/manifests/" + manifest)
+ assert.Equal(t, err, nil, "Error should be nil")
+ assert.Equal(t, headResponse.StatusCode(), http.StatusNotFound, "response status code should return 404")
- resp, err = resty.R().
- SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))).
- SetHeader("Content-Type", "application/octet-stream").
- SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
- SetQueryParam("digest", digest).
- SetBody(blob).
- Put(baseURL + loc)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ getResponse, err := client.R().SetBasicAuth(username, passphrase).
+ Get(baseURL + "/v2/" + testcase.destImageName + "/manifests/" + manifest)
+ assert.Equal(t, err, nil, "Error should be nil")
+ assert.Equal(t, getResponse.StatusCode(), http.StatusNotFound, "response status code should return 404")
+ }
- resp, err = resty.R().
- SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
- Get(baseURL + "/v2/" + AuthorizedNamespace + "/tags/list")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ blobList := getAllBlobs(path.Join(testImagesDir, testcase.srcImageName))
- authorizationHeader = authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
- resp, err = resty.R().
- SetQueryParam("service", authorizationHeader.Service).
- SetQueryParam("scope", authorizationHeader.Scope).
- Get(authorizationHeader.Realm)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- err = json.Unmarshal(resp.Body(), &goodToken)
- So(err, ShouldBeNil)
+ for _, blob := range blobList {
+ // Get request of blob
+ headResponse, err := client.R().
+ SetBasicAuth(username, passphrase).
+ Head(baseURL + "/v2/" + testcase.destImageName + "/blobs/sha256:" + blob)
- resp, err = resty.R().
- SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
- Get(baseURL + "/v2/" + AuthorizedNamespace + "/tags/list")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ assert.Equal(t, err, nil, "Should not be nil")
+ assert.NotEqual(t, headResponse.StatusCode(), http.StatusInternalServerError,
+ "internal server error should not occurred")
- resp, err = resty.R().
- Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ getResponse, err := client.R().
+ SetBasicAuth(username, passphrase).
+ Get(baseURL + "/v2/" + testcase.destImageName + "/blobs/sha256:" + blob)
- authorizationHeader = authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
- resp, err = resty.R().
- SetQueryParam("service", authorizationHeader.Service).
- SetQueryParam("scope", authorizationHeader.Scope).
- Get(authorizationHeader.Realm)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- var badToken authutils.AccessTokenResponse
- err = json.Unmarshal(resp.Body(), &badToken)
- So(err, ShouldBeNil)
+ assert.Equal(t, err, nil, "Should not be nil")
+ assert.NotEqual(t, getResponse.StatusCode(), http.StatusInternalServerError,
+ "internal server error should not occurred")
- resp, err = resty.R().
- SetHeader("Authorization", fmt.Sprintf("Bearer %s", badToken.AccessToken)).
- Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- })
-}
+ blobPath := path.Join(testImagesDir, testcase.srcImageName, "blobs/sha256", blob)
-func TestBearerAuthWrongAuthorizer(t *testing.T) {
- Convey("Make a new authorizer", t, func() {
- port := test.GetFreePort()
+ buf, err := os.ReadFile(blobPath)
+ if err != nil {
+ panic(err)
+ }
- conf := config.New()
- conf.HTTP.Port = port
- conf.HTTP.Auth = &config.AuthConfig{
- Bearer: &config.BearerConfig{
- Cert: "bla",
- Realm: "blabla",
- Service: "blablabla",
- },
- }
- ctlr := makeController(conf, t.TempDir())
+ // Post request of blob
+ postResponse, err := client.R().
+ SetHeader("Content-type", "application/octet-stream").
+ SetBasicAuth(username, passphrase).
+ SetBody(buf).Post(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/")
- So(func() {
- api.AuthHandler(ctlr)
- }, ShouldPanic)
- })
-}
+ assert.Equal(t, err, nil, "Error should be nil")
+ assert.NotEqual(t, postResponse.StatusCode(), http.StatusInternalServerError,
+ "response status code should not return 500")
-func TestBearerAuthWithAllowReadAccess(t *testing.T) {
- Convey("Make a new controller", t, func() {
- authTestServer := authutils.MakeAuthTestServer(ServerKey, UnauthorizedNamespace)
- defer authTestServer.Close()
+ // Post request with query parameter
+ if run%2 == 0 {
+ postResponse, err = client.R().
+ SetHeader("Content-type", "application/octet-stream").
+ SetBasicAuth(username, passphrase).
+ SetBody(buf).
+ Post(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/")
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
+ assert.Equal(t, err, nil, "Error should be nil")
+ assert.NotEqual(t, postResponse.StatusCode(), http.StatusInternalServerError,
+ "response status code should not return 500")
- conf := config.New()
- conf.HTTP.Port = port
+ var sessionID string
+ sessionIDList := postResponse.Header().Values("Blob-Upload-UUID")
+ if len(sessionIDList) == 0 {
+ location := postResponse.Header().Values("Location")
+ firstLocation := location[0]
+ splitLocation := strings.Split(firstLocation, "/")
+ sessionID = splitLocation[len(splitLocation)-1]
+ } else {
+ sessionID = sessionIDList[0]
+ }
- aurl, err := url.Parse(authTestServer.URL)
- So(err, ShouldBeNil)
+ file, err := os.Open(blobPath)
+ if err != nil {
+ panic(err)
+ }
- conf.HTTP.Auth = &config.AuthConfig{
- Bearer: &config.BearerConfig{
- Cert: ServerCert,
- Realm: authTestServer.URL + "/auth/token",
- Service: aurl.Host,
- },
- }
- ctlr := makeController(conf, t.TempDir())
+ defer file.Close()
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Repositories: config.Repositories{
- AuthorizationAllRepos: config.PolicyGroup{
- AnonymousPolicy: []string{"read"},
- },
- },
- }
+ reader := bufio.NewReader(file)
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ buf := make([]byte, 5*1024*1024)
- blob := []byte("hello, blob!")
- digest := godigest.FromBytes(blob).String()
+ if run%4 == 0 {
+ readContent := 0
+ for {
+ nbytes, err := reader.Read(buf)
+ if err != nil {
+ if goerrors.Is(err, io.EOF) {
+ break
+ }
+ panic(err)
+ }
+ // Patch request of blob
- resp, err := resty.R().Get(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ patchResponse, err := client.R().
+ SetBody(buf[0:nbytes]).
+ SetHeader("Content-Type", "application/octet-stream").
+ SetHeader("Content-Length", fmt.Sprintf("%d", nbytes)).
+ SetHeader("Content-Range", fmt.Sprintf("%d", readContent)+"-"+fmt.Sprintf("%d", readContent+nbytes-1)).
+ SetBasicAuth(username, passphrase).
+ Patch(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/" + sessionID)
- authorizationHeader := authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
- resp, err = resty.R().
- SetQueryParam("service", authorizationHeader.Service).
- SetQueryParam("scope", authorizationHeader.Scope).
- Get(authorizationHeader.Realm)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- var goodToken authutils.AccessTokenResponse
- err = json.Unmarshal(resp.Body(), &goodToken)
- So(err, ShouldBeNil)
+ assert.Equal(t, err, nil, "Error should be nil")
+ assert.NotEqual(t, patchResponse.StatusCode(), http.StatusInternalServerError,
+ "response status code should not return 500")
- resp, err = resty.R().
- SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
- Get(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ readContent += nbytes
+ }
+ } else {
+ for {
+ nbytes, err := reader.Read(buf)
+ if err != nil {
+ if goerrors.Is(err, io.EOF) {
+ break
+ }
+ panic(err)
+ }
+ // Patch request of blob
- resp, err = resty.R().Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ patchResponse, err := client.R().SetBody(buf[0:nbytes]).SetHeader("Content-type", "application/octet-stream").
+ SetBasicAuth(username, passphrase).
+ Patch(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/" + sessionID)
+ if err != nil {
+ panic(err)
+ }
- authorizationHeader = authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
- resp, err = resty.R().
- SetQueryParam("service", authorizationHeader.Service).
- SetQueryParam("scope", authorizationHeader.Scope).
- Get(authorizationHeader.Realm)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- err = json.Unmarshal(resp.Body(), &goodToken)
- So(err, ShouldBeNil)
-
- resp, err = resty.R().
- SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
- Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
- loc := resp.Header().Get("Location")
-
- resp, err = resty.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.StatusUnauthorized)
-
- authorizationHeader = authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
- resp, err = resty.R().
- SetQueryParam("service", authorizationHeader.Service).
- SetQueryParam("scope", authorizationHeader.Scope).
- Get(authorizationHeader.Realm)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- err = json.Unmarshal(resp.Body(), &goodToken)
- So(err, ShouldBeNil)
+ assert.Equal(t, err, nil, "Error should be nil")
+ assert.NotEqual(t, patchResponse.StatusCode(), http.StatusInternalServerError,
+ "response status code should not return 500")
+ }
+ }
+ } else {
+ postResponse, err = client.R().
+ SetHeader("Content-type", "application/octet-stream").
+ SetBasicAuth(username, passphrase).
+ SetBody(buf).SetQueryParam("digest", "sha256:"+blob).
+ Post(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/")
- resp, err = resty.R().
- SetHeader("Content-Length", fmt.Sprintf("%d", len(blob))).
- SetHeader("Content-Type", "application/octet-stream").
- SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
- SetQueryParam("digest", digest).
- SetBody(blob).
- Put(baseURL + loc)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ assert.Equal(t, err, nil, "Error should be nil")
+ assert.NotEqual(t, postResponse.StatusCode(), http.StatusInternalServerError,
+ "response status code should not return 500")
+ }
- resp, err = resty.R().
- SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
- Get(baseURL + "/v2/" + AuthorizedNamespace + "/tags/list")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ headResponse, err = client.R().
+ SetBasicAuth(username, passphrase).
+ Head(baseURL + "/v2/" + testcase.destImageName + "/blobs/sha256:" + blob)
- authorizationHeader = authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
- resp, err = resty.R().
- SetQueryParam("service", authorizationHeader.Service).
- SetQueryParam("scope", authorizationHeader.Scope).
- Get(authorizationHeader.Realm)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- err = json.Unmarshal(resp.Body(), &goodToken)
- So(err, ShouldBeNil)
+ assert.Equal(t, err, nil, "Should not be nil")
+ assert.NotEqual(t, headResponse.StatusCode(), http.StatusInternalServerError, "response should return success code")
- resp, err = resty.R().
- SetHeader("Authorization", fmt.Sprintf("Bearer %s", goodToken.AccessToken)).
- Get(baseURL + "/v2/" + AuthorizedNamespace + "/tags/list")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ getResponse, err = client.R().
+ SetBasicAuth(username, passphrase).
+ Get(baseURL + "/v2/" + testcase.destImageName + "/blobs/sha256:" + blob)
- resp, err = resty.R().
- Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ assert.Equal(t, err, nil, "Should not be nil")
+ assert.NotEqual(t, getResponse.StatusCode(), http.StatusInternalServerError, "response should return success code")
+ }
- authorizationHeader = authutils.ParseBearerAuthHeader(resp.Header().Get("WWW-Authenticate"))
- resp, err = resty.R().
- SetQueryParam("service", authorizationHeader.Service).
- SetQueryParam("scope", authorizationHeader.Scope).
- Get(authorizationHeader.Realm)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- var badToken authutils.AccessTokenResponse
- err = json.Unmarshal(resp.Body(), &badToken)
- So(err, ShouldBeNil)
+ tagResponse, err = client.R().SetBasicAuth(username, passphrase).
+ Get(baseURL + "/v2/" + testcase.destImageName + "/tags/list")
+ assert.Equal(t, err, nil, "Error should be nil")
+ assert.Equal(t, tagResponse.StatusCode(), http.StatusOK, "response status code should return success code")
- resp, err = resty.R().
- SetHeader("Authorization", fmt.Sprintf("Bearer %s", badToken.AccessToken)).
- Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- })
+ repoResponse, err := client.R().SetBasicAuth(username, passphrase).
+ Get(baseURL + constants.RoutePrefix + constants.ExtCatalogPrefix)
+ assert.Equal(t, err, nil, "Error should be nil")
+ assert.Equal(t, repoResponse.StatusCode(), http.StatusOK, "response status code should return success code")
+ })
+ }
}
-func TestNewRelyingPartyOIDC(t *testing.T) {
- Convey("Test NewRelyingPartyOIDC", t, func() {
+func TestHardLink(t *testing.T) {
+ Convey("Validate hard link", t, func() {
+ port := test.GetFreePort()
conf := config.New()
+ conf.HTTP.Port = port
- mockOIDCServer, err := authutils.MockOIDCRun()
+ dir := t.TempDir()
+
+ err := os.Chmod(dir, 0o400)
if err != nil {
panic(err)
}
- defer func() {
- err := mockOIDCServer.Shutdown()
- if err != nil {
- panic(err)
- }
- }()
-
- mockOIDCConfig := mockOIDCServer.Config()
+ subDir := t.TempDir()
- conf.HTTP.Auth = &config.AuthConfig{
- OpenID: &config.OpenIDConfig{
- Providers: map[string]config.OpenIDProviderConfig{
- "oidc": {
- ClientID: mockOIDCConfig.ClientID,
- ClientSecret: mockOIDCConfig.ClientSecret,
- KeyPath: "",
- Issuer: mockOIDCConfig.Issuer,
- Scopes: []string{"openid", "email"},
- },
- },
- },
+ err = os.Chmod(subDir, 0o400)
+ if err != nil {
+ panic(err)
}
- Convey("provider not found in config", func() {
- So(func() { _ = api.NewRelyingPartyOIDC(conf, "notDex") }, ShouldPanic)
- })
-
- Convey("key path not found on disk", func() {
- oidcProviderCfg := conf.HTTP.Auth.OpenID.Providers["oidc"]
- oidcProviderCfg.KeyPath = "path/to/file"
- conf.HTTP.Auth.OpenID.Providers["oidc"] = oidcProviderCfg
-
- So(func() { _ = api.NewRelyingPartyOIDC(conf, "oidc") }, ShouldPanic)
- })
-
- Convey("https callback", func() {
- conf.HTTP.TLS = &config.TLSConfig{
- Cert: ServerCert,
- Key: ServerKey,
- }
+ ctlr := makeController(conf, dir)
+ subPaths := make(map[string]config.StorageConfig)
- rp := api.NewRelyingPartyOIDC(conf, "oidc")
- So(rp, ShouldNotBeNil)
- })
+ subPaths["/a"] = config.StorageConfig{RootDirectory: subDir, Dedupe: true}
+ ctlr.Config.Storage.SubPaths = subPaths
- Convey("no client secret in config", func() {
- oidcProvider := conf.HTTP.Auth.OpenID.Providers["oidc"]
- oidcProvider.ClientSecret = ""
- conf.HTTP.Auth.OpenID.Providers["oidc"] = oidcProvider
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- rp := api.NewRelyingPartyOIDC(conf, "oidc")
- So(rp, ShouldNotBeNil)
- })
+ err = os.Chmod(dir, 0o644)
+ if err != nil {
+ panic(err)
+ }
- Convey("provider issuer unreachable", func() {
- oidcProvider := conf.HTTP.Auth.OpenID.Providers["oidc"]
- oidcProvider.Issuer = ""
- conf.HTTP.Auth.OpenID.Providers["oidc"] = oidcProvider
+ err = os.Chmod(subDir, 0o644)
+ if err != nil {
+ panic(err)
+ }
- So(func() { _ = api.NewRelyingPartyOIDC(conf, "oidc") }, ShouldPanic)
- })
+ So(ctlr.Config.Storage.Dedupe, ShouldEqual, false)
})
}
-func TestOpenIDMiddleware(t *testing.T) {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- defaultVal := true
+func TestImageSignatures(t *testing.T) {
+ Convey("Validate signatures", t, func() {
+ // start a new server
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
- conf := config.New()
- conf.HTTP.Port = port
+ conf := config.New()
+ conf.HTTP.Port = port
- testCases := []struct {
- testCaseName string
- address string
- externalURL string
- }{
- {
- address: "0.0.0.0",
- externalURL: fmt.Sprintf("http://%s", net.JoinHostPort(conf.HTTP.Address, conf.HTTP.Port)),
- testCaseName: "with ExternalURL provided in config",
- },
- {
- address: "127.0.0.1",
- externalURL: "",
- testCaseName: "without ExternalURL provided in config",
- },
- }
+ dir := t.TempDir()
+ ctlr := makeController(conf, dir)
+ cm := test.NewControllerManager(ctlr)
+ // this blocks
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- // need a username different than ldap one, to test both logic
- content := fmt.Sprintf("%s:$2y$05$hlbSXDp6hzDLu6VwACS39ORvVRpr3OMR4RlJ31jtlaOEGnPjKZI1m\n", htpasswdUsername)
- htpasswdPath := test.MakeHtpasswdFileFromString(content)
+ repoName := "signed-repo"
+ img := CreateRandomImage()
+ content := img.ManifestDescriptor.Data
+ digest := img.ManifestDescriptor.Digest
- defer os.Remove(htpasswdPath)
+ err := UploadImage(img, baseURL, repoName, "1.0")
+ So(err, ShouldBeNil)
- ldapServer := newTestLDAPServer()
- port = test.GetFreePort()
+ Convey("Validate cosign signatures", func() {
+ cwd, err := os.Getwd()
+ So(err, ShouldBeNil)
+ defer func() { _ = os.Chdir(cwd) }()
+ tdir := t.TempDir()
+ _ = os.Chdir(tdir)
- ldapPort, err := strconv.Atoi(port)
- if err != nil {
- panic(err)
- }
+ // generate a keypair
+ os.Setenv("COSIGN_PASSWORD", "")
+ err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil)
+ So(err, ShouldBeNil)
- ldapServer.Start(ldapPort)
- defer ldapServer.Stop()
+ annotations := []string{"tag=1.0"}
- mockOIDCServer, err := authutils.MockOIDCRun()
- if err != nil {
- panic(err)
- }
-
- defer func() {
- err := mockOIDCServer.Shutdown()
- if err != nil {
- panic(err)
- }
- }()
-
- mockOIDCConfig := mockOIDCServer.Config()
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- LDAP: &config.LDAPConfig{
- Insecure: true,
- Address: LDAPAddress,
- Port: ldapPort,
- BindDN: LDAPBindDN,
- BindPassword: LDAPBindPassword,
- BaseDN: LDAPBaseDN,
- UserAttribute: "uid",
- },
- OpenID: &config.OpenIDConfig{
- Providers: map[string]config.OpenIDProviderConfig{
- "oidc": {
- ClientID: mockOIDCConfig.ClientID,
- ClientSecret: mockOIDCConfig.ClientSecret,
- KeyPath: "",
- Issuer: mockOIDCConfig.Issuer,
- Scopes: []string{"openid", "email"},
- },
- // just for the constructor coverage
- "github": {
- ClientID: mockOIDCConfig.ClientID,
- ClientSecret: mockOIDCConfig.ClientSecret,
- KeyPath: "",
- Issuer: mockOIDCConfig.Issuer,
- Scopes: []string{"openid", "email"},
+ // sign the image
+ err = sign.SignCmd(&options.RootOptions{Verbose: true, Timeout: 1 * time.Minute},
+ options.KeyOpts{KeyRef: path.Join(tdir, "cosign.key"), PassFunc: generate.GetPass},
+ options.SignOptions{
+ Registry: options.RegistryOptions{AllowInsecure: true},
+ AnnotationOptions: options.AnnotationOptions{Annotations: annotations},
+ Upload: true,
},
- },
- },
- }
+ []string{fmt.Sprintf("localhost:%s/%s@%s", port, repoName, digest.String())})
+ So(err, ShouldBeNil)
- searchConfig := &extconf.SearchConfig{
- BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
- }
+ // verify the image
+ aopts := &options.AnnotationOptions{Annotations: annotations}
+ amap, err := aopts.AnnotationsMap()
+ So(err, ShouldBeNil)
+ vrfy := verify.VerifyCommand{
+ RegistryOptions: options.RegistryOptions{AllowInsecure: true},
+ CheckClaims: true,
+ KeyRef: path.Join(tdir, "cosign.pub"),
+ Annotations: amap,
+ IgnoreTlog: true,
+ }
+ err = vrfy.Exec(context.TODO(), []string{fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")})
+ So(err, ShouldBeNil)
- // UI is enabled because we also want to test access on the mgmt route
- uiConfig := &extconf.UIConfig{
- BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
- }
+ // verify the image with incorrect tag
+ aopts = &options.AnnotationOptions{Annotations: []string{"tag=2.0"}}
+ amap, err = aopts.AnnotationsMap()
+ So(err, ShouldBeNil)
+ vrfy = verify.VerifyCommand{
+ RegistryOptions: options.RegistryOptions{AllowInsecure: true},
+ CheckClaims: true,
+ KeyRef: path.Join(tdir, "cosign.pub"),
+ Annotations: amap,
+ IgnoreTlog: true,
+ }
+ err = vrfy.Exec(context.TODO(), []string{fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")})
+ So(err, ShouldNotBeNil)
- conf.Extensions = &extconf.ExtensionConfig{
- Search: searchConfig,
- UI: uiConfig,
- }
+ // verify the image with incorrect key
+ aopts = &options.AnnotationOptions{Annotations: []string{"tag=1.0"}}
+ amap, err = aopts.AnnotationsMap()
+ So(err, ShouldBeNil)
+ vrfy = verify.VerifyCommand{
+ CheckClaims: true,
+ RegistryOptions: options.RegistryOptions{AllowInsecure: true},
+ KeyRef: path.Join(tdir, "cosign.key"),
+ Annotations: amap,
+ IgnoreTlog: true,
+ }
+ err = vrfy.Exec(context.TODO(), []string{fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")})
+ So(err, ShouldNotBeNil)
- ctlr := api.NewController(conf)
+ // generate another keypair
+ err = os.Remove(path.Join(tdir, "cosign.pub"))
+ So(err, ShouldBeNil)
+ err = os.Remove(path.Join(tdir, "cosign.key"))
+ So(err, ShouldBeNil)
- for _, testcase := range testCases {
- t.Run(testcase.testCaseName, func(t *testing.T) {
- Convey("make controller", t, func() {
- dir := t.TempDir()
-
- ctlr.Config.Storage.RootDirectory = dir
- ctlr.Config.HTTP.ExternalURL = testcase.externalURL
- ctlr.Config.HTTP.Address = testcase.address
- cm := test.NewControllerManager(ctlr)
-
- cm.StartServer()
- defer cm.StopServer()
- test.WaitTillServerReady(baseURL)
-
- Convey("browser client requests", func() {
- Convey("login with no provider supplied", func() {
- client := resty.New()
- client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
- // first login user
- resp, err := client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- SetQueryParam("provider", "unknown").
- Get(baseURL + constants.LoginPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
- })
-
- //nolint: dupl
- Convey("make sure sessions are not used without UI header value", func() {
- sessionsNo, err := getNumberOfSessions(conf.Storage.RootDirectory)
- So(err, ShouldBeNil)
- So(sessionsNo, ShouldEqual, 0)
-
- client := resty.New()
-
- // without header should not create session
- resp, err := client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- sessionsNo, err = getNumberOfSessions(conf.Storage.RootDirectory)
- So(err, ShouldBeNil)
- So(sessionsNo, ShouldEqual, 0)
-
- client.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
-
- resp, err = client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- sessionsNo, err = getNumberOfSessions(conf.Storage.RootDirectory)
- So(err, ShouldBeNil)
- So(sessionsNo, ShouldEqual, 1)
-
- // set cookies
- client.SetCookies(resp.Cookies())
-
- // should get same cookie
- resp, err = client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- sessionsNo, err = getNumberOfSessions(conf.Storage.RootDirectory)
- So(err, ShouldBeNil)
- So(sessionsNo, ShouldEqual, 1)
-
- resp, err = client.R().
- SetBasicAuth(htpasswdUsername, passphrase).
- Get(baseURL + constants.FullMgmt)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- client.SetCookies(resp.Cookies())
-
- // call endpoint with session, without credentials, (added to client after previous request)
- resp, err = client.R().
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- resp, err = client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- sessionsNo, err = getNumberOfSessions(conf.Storage.RootDirectory)
- So(err, ShouldBeNil)
- So(sessionsNo, ShouldEqual, 1)
- })
-
- Convey("login with openid and get catalog with session", func() {
- client := resty.New()
- client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
- client.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
-
- Convey("with callback_ui value provided", func() {
- // first login user
- resp, err := client.R().
- SetQueryParam("provider", "oidc").
- SetQueryParam("callback_ui", baseURL+"/v2/").
- Get(baseURL + constants.LoginPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- })
-
- // first login user
- resp, err := client.R().
- SetQueryParam("provider", "oidc").
- Get(baseURL + constants.LoginPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
-
- client.SetCookies(resp.Cookies())
-
- // call endpoint with session (added to client after previous request)
- resp, err = client.R().
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- // logout with options method for coverage
- resp, err = client.R().
- Options(baseURL + constants.LogoutPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
-
- // logout user
- resp, err = client.R().
- Post(baseURL + constants.LogoutPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- // calling endpoint should fail with unauthorized access
- resp, err = client.R().
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- })
-
- //nolint: dupl
- Convey("login with basic auth(htpasswd) and get catalog with session", func() {
- client := resty.New()
- client.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
-
- // without creds, should get access error
- resp, err := client.R().Get(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- var e apiErr.Error
- err = json.Unmarshal(resp.Body(), &e)
- So(err, ShouldBeNil)
-
- // first login user
- // with creds, should get expected status code
- resp, err = client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- resp, err = client.R().SetBasicAuth(htpasswdUsername, passphrase).Get(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- resp, err = client.R().
- SetBasicAuth(htpasswdUsername, passphrase).
- Get(baseURL + constants.FullMgmt)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- client.SetCookies(resp.Cookies())
-
- // call endpoint with session, without credentials, (added to client after previous request)
- resp, err = client.R().
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- resp, err = client.R().
- Get(baseURL + constants.FullMgmt)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- // logout user
- resp, err = client.R().
- Post(baseURL + constants.LogoutPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- // calling endpoint should fail with unauthorized access
- resp, err = client.R().
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- })
-
- //nolint: dupl
- Convey("login with ldap and get catalog", func() {
- client := resty.New()
- client.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
-
- // without creds, should get access error
- resp, err := client.R().Get(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- var e apiErr.Error
- err = json.Unmarshal(resp.Body(), &e)
- So(err, ShouldBeNil)
-
- // first login user
- // with creds, should get expected status code
- resp, err = client.R().SetBasicAuth(username, passphrase).Get(baseURL)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- resp, err = client.R().SetBasicAuth(username, passphrase).Get(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- resp, err = client.R().
- SetBasicAuth(username, passphrase).
- Get(baseURL + constants.FullMgmt)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- client.SetCookies(resp.Cookies())
-
- // call endpoint with session, without credentials, (added to client after previous request)
- resp, err = client.R().
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- resp, err = client.R().
- Get(baseURL + constants.FullMgmt)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- // logout user
- resp, err = client.R().
- Post(baseURL + constants.LogoutPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- // calling endpoint should fail with unauthorized access
- resp, err = client.R().
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- })
-
- Convey("unauthenticated catalog request", func() {
- client := resty.New()
-
- // mgmt should work both unauthenticated and authenticated
- resp, err := client.R().
- Get(baseURL + constants.FullMgmt)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- // call endpoint without session
- resp, err = client.R().
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- })
- })
- })
+ os.Setenv("COSIGN_PASSWORD", "")
+ err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil)
+ So(err, ShouldBeNil)
+
+ // verify the image with incorrect key
+ aopts = &options.AnnotationOptions{Annotations: []string{"tag=1.0"}}
+ amap, err = aopts.AnnotationsMap()
+ So(err, ShouldBeNil)
+ vrfy = verify.VerifyCommand{
+ CheckClaims: true,
+ RegistryOptions: options.RegistryOptions{AllowInsecure: true},
+ KeyRef: path.Join(tdir, "cosign.pub"),
+ Annotations: amap,
+ IgnoreTlog: true,
+ }
+ err = vrfy.Exec(context.TODO(), []string{fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")})
+ So(err, ShouldNotBeNil)
})
- }
-}
-func TestIsOpenIDEnabled(t *testing.T) {
- Convey("make oidc server", t, func() {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
+ Convey("Validate notation signatures", func() {
+ cwd, err := os.Getwd()
+ So(err, ShouldBeNil)
+ defer func() { _ = os.Chdir(cwd) }()
+ tdir := t.TempDir()
+ _ = os.Chdir(tdir)
- conf := config.New()
- conf.HTTP.Port = port
+ signature.NotationPathLock.Lock()
+ defer signature.NotationPathLock.Unlock()
- mockOIDCServer, err := authutils.MockOIDCRun()
- if err != nil {
- panic(err)
- }
+ signature.LoadNotationPath(tdir)
- defer func() {
- err := mockOIDCServer.Shutdown()
- if err != nil {
- panic(err)
- }
- }()
+ err = signature.GenerateNotationCerts(tdir, "good")
+ So(err, ShouldBeNil)
- 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"},
- },
- },
- },
- }
+ err = signature.GenerateNotationCerts(tdir, "bad")
+ So(err, ShouldBeNil)
- ctlr := api.NewController(conf)
+ image := fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")
+ err = signature.SignWithNotation("good", image, tdir)
+ So(err, ShouldBeNil)
- ctlr.Config.Storage.RootDirectory = rootDir
+ err = signature.VerifyWithNotation(image, tdir)
+ So(err, ShouldBeNil)
- cm := test.NewControllerManager(ctlr)
+ // check list
+ sigs, err := signature.ListNotarySignatures(image, tdir)
+ So(len(sigs), ShouldEqual, 1)
+ So(err, ShouldBeNil)
- cm.StartServer()
- defer cm.StopServer()
- test.WaitTillServerReady(baseURL)
+ // check unsupported manifest media type
+ resp, err := resty.R().SetHeader("Content-Type", "application/vnd.unsupported.image.manifest.v1+json").
+ SetBody(content).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusUnsupportedMediaType)
- resp, err := resty.R().
- Get(baseURL + "/v2/")
+ // check invalid content with artifact media type
+ resp, err = resty.R().SetHeader("Content-Type", artifactspec.MediaTypeArtifactManifest).
+ SetBody([]byte("bogus")).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- })
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
- Convey("Unsupported provider", func() {
- mockOIDCConfig := mockOIDCServer.Config()
- conf.HTTP.Auth = &config.AuthConfig{
- OpenID: &config.OpenIDConfig{
- Providers: map[string]config.OpenIDProviderConfig{
- "invalidProvider": {
- ClientID: mockOIDCConfig.ClientID,
- ClientSecret: mockOIDCConfig.ClientSecret,
- KeyPath: "",
- Issuer: mockOIDCConfig.Issuer,
- Scopes: []string{"email", "groups"},
- },
- },
- },
- }
+ Convey("Validate corrupted signature", func() {
+ // verify with corrupted signature
+ resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
+ fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String()))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ var refs ispec.Index
+ err = json.Unmarshal(resp.Body(), &refs)
+ So(err, ShouldBeNil)
+ So(len(refs.Manifests), ShouldEqual, 1)
+ err = os.WriteFile(path.Join(dir, repoName, "blobs",
+ strings.ReplaceAll(refs.Manifests[0].Digest.String(), ":", "/")), []byte("corrupt"), 0o600)
+ So(err, ShouldBeNil)
+ resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
+ fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String()))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
- ctlr := api.NewController(conf)
+ err = signature.VerifyWithNotation(image, tdir)
+ So(err, ShouldNotBeNil)
+ })
- ctlr.Config.Storage.RootDirectory = rootDir
+ Convey("Validate deleted signature", func() {
+ // verify with corrupted signature
+ resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
+ fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String()))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ var refs ispec.Index
+ err = json.Unmarshal(resp.Body(), &refs)
+ So(err, ShouldBeNil)
+ So(len(refs.Manifests), ShouldEqual, 1)
+ err = os.Remove(path.Join(dir, repoName, "blobs",
+ strings.ReplaceAll(refs.Manifests[0].Digest.String(), ":", "/")))
+ So(err, ShouldBeNil)
+ resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
+ fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String()))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- cm := test.NewControllerManager(ctlr)
+ err = signature.VerifyWithNotation(image, tdir)
+ So(err, ShouldNotBeNil)
+ })
+ })
- cm.StartServer()
- defer cm.StopServer()
- test.WaitTillServerReady(baseURL)
+ Convey("GetOrasReferrers", func() {
+ // cover error paths
+ resp, err := resty.R().Get(
+ fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, "badRepo", "badDigest"))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- // 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/")
+ resp, err = resty.R().Get(
+ fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, "badDigest"))
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+
+ resp, err = resty.R().Get(
+ fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String()))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+
+ resp, err = resty.R().SetQueryParam("artifactType", "badArtifact").Get(
+ fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String()))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+
+ resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
+ fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, "badRepo", digest.String()))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
})
})
}
-func TestAuthnSessionErrors(t *testing.T) {
- Convey("make controller", t, func() {
+func TestManifestValidation(t *testing.T) {
+ Convey("Validate manifest", t, func() {
+ // start a new server
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
- defaultVal := true
conf := config.New()
conf.HTTP.Port = port
- invalidSessionID := "sessionID"
-
- // need a username different than ldap one, to test both logic
- content := fmt.Sprintf("%s:$2y$05$hlbSXDp6hzDLu6VwACS39ORvVRpr3OMR4RlJ31jtlaOEGnPjKZI1m\n", htpasswdUsername)
-
- htpasswdPath := test.MakeHtpasswdFileFromString(content)
- defer os.Remove(htpasswdPath)
- ldapServer := newTestLDAPServer()
- port = test.GetFreePort()
+ dir := t.TempDir()
+ ctlr := makeController(conf, dir)
+ cm := test.NewControllerManager(ctlr)
+ // this blocks
+ cm.StartServer()
+ time.Sleep(1000 * time.Millisecond)
+ defer cm.StopServer()
- ldapPort, err := strconv.Atoi(port)
- if err != nil {
- panic(err)
- }
+ repoName := "validation"
+ blobContent := []byte("this is a blob")
+ blobDigest := godigest.FromBytes(blobContent)
+ So(blobDigest, ShouldNotBeNil)
- ldapServer.Start(ldapPort)
- defer ldapServer.Stop()
+ img := CreateRandomImage()
+ content := img.ManifestDescriptor.Data
+ digest := img.ManifestDescriptor.Digest
+ configDigest := img.ConfigDescriptor.Digest
+ configBlob := img.ConfigDescriptor.Data
- mockOIDCServer, err := authutils.MockOIDCRun()
- if err != nil {
- panic(err)
- }
+ err := UploadImage(img, baseURL, repoName, "1.0")
+ So(err, ShouldBeNil)
- defer func() {
- err := mockOIDCServer.Shutdown()
- if err != nil {
- panic(err)
- }
- }()
-
- rootDir := t.TempDir()
-
- mockOIDCConfig := mockOIDCServer.Config()
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- LDAP: &config.LDAPConfig{
- Insecure: true,
- Address: LDAPAddress,
- Port: ldapPort,
- BindDN: LDAPBindDN,
- BindPassword: LDAPBindPassword,
- BaseDN: LDAPBaseDN,
- UserAttribute: "uid",
- },
- OpenID: &config.OpenIDConfig{
- Providers: map[string]config.OpenIDProviderConfig{
- "oidc": {
- ClientID: mockOIDCConfig.ClientID,
- ClientSecret: mockOIDCConfig.ClientSecret,
- KeyPath: "",
- Issuer: mockOIDCConfig.Issuer,
- Scopes: []string{"email", "groups"},
- },
+ Convey("empty layers should pass validation", func() {
+ // create a manifest
+ manifest := ispec.Manifest{
+ Config: ispec.Descriptor{
+ MediaType: ispec.MediaTypeImageConfig,
+ Size: int64(len(configBlob)),
+ Digest: configDigest,
},
- },
- }
-
- uiConfig := &extconf.UIConfig{
- BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
- }
-
- searchConfig := &extconf.SearchConfig{
- BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
- }
-
- conf.Extensions = &extconf.ExtensionConfig{
- UI: uiConfig,
- Search: searchConfig,
- }
-
- ctlr := api.NewController(conf)
-
- ctlr.Config.Storage.RootDirectory = rootDir
-
- cm := test.NewControllerManager(ctlr)
-
- cm.StartServer()
- defer cm.StopServer()
- test.WaitTillServerReady(baseURL)
-
- Convey("trigger basic authn middle(htpasswd) error", func() {
- client := resty.New()
-
- ctlr.MetaDB = mocks.MetaDBMock{
- SetUserGroupsFn: func(ctx context.Context, groups []string) error {
- return ErrUnexpectedError
+ Layers: []ispec.Descriptor{},
+ Annotations: map[string]string{
+ "key": "val",
},
}
+ manifest.SchemaVersion = 2
- resp, err := client.R().
- SetBasicAuth(htpasswdUsername, passphrase).
- Get(baseURL + "/v2/_catalog")
+ mcontent, err := json.Marshal(manifest)
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
- })
- Convey("trigger basic authn middle(ldap) error", func() {
- client := resty.New()
+ resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ })
- ctlr.MetaDB = mocks.MetaDBMock{
- SetUserGroupsFn: func(ctx context.Context, groups []string) error {
- return ErrUnexpectedError
+ Convey("empty layers and schemaVersion missing should fail validation", func() {
+ // create a manifest
+ manifest := ispec.Manifest{
+ Config: ispec.Descriptor{
+ MediaType: ispec.MediaTypeImageConfig,
+ Size: int64(len(configBlob)),
+ Digest: configDigest,
+ },
+ Layers: []ispec.Descriptor{},
+ Annotations: map[string]string{
+ "key": "val",
},
}
- resp, err := client.R().
- SetBasicAuth(username, passphrase).
- Get(baseURL + "/v2/_catalog")
+ mcontent, err := json.Marshal(manifest)
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
- })
- Convey("trigger updateUserData error", func() {
- client := resty.New()
- client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+ resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+ })
- ctlr.MetaDB = mocks.MetaDBMock{
- SetUserGroupsFn: func(ctx context.Context, groups []string) error {
- return ErrUnexpectedError
+ Convey("missing layer should fail validation", func() {
+ missingLayer := []byte("missing layer")
+ missingLayerDigest := godigest.FromBytes(missingLayer)
+ // create a manifest
+ manifest := ispec.Manifest{
+ Config: ispec.Descriptor{
+ MediaType: ispec.MediaTypeImageConfig,
+ Size: int64(len(configBlob)),
+ Digest: configDigest,
+ },
+ Layers: []ispec.Descriptor{
+ {
+ MediaType: "application/vnd.oci.image.layer.v1.tar",
+ Digest: digest,
+ Size: int64(len(content)),
+ },
+ {
+ MediaType: "application/vnd.oci.image.layer.v1.tar",
+ Digest: missingLayerDigest,
+ Size: int64(len(missingLayer)),
+ },
+ },
+ Annotations: map[string]string{
+ "key": "val",
},
}
+ manifest.SchemaVersion = 2
- resp, err := client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- SetQueryParam("provider", "oidc").
- Get(baseURL + constants.LoginPath)
+ mcontent, err := json.Marshal(manifest)
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
- })
- Convey("trigger session middle metaDB errors", func() {
- client := resty.New()
- client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
-
- user := mockoidc.DefaultUser()
- user.Groups = []string{"group1", "group2"}
-
- mockOIDCServer.QueueUser(user)
-
- ctlr.MetaDB = mocks.MetaDBMock{}
-
- // first login user
- resp, err := client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- SetQueryParam("provider", "oidc").
- Get(baseURL + constants.LoginPath)
+ resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
-
- Convey("trigger session middle error internal server error", func() {
- cookies := resp.Cookies()
-
- client.SetCookies(cookies)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+ })
- ctlr.MetaDB = mocks.MetaDBMock{
- GetUserGroupsFn: func(ctx context.Context) ([]string, error) {
- return []string{}, ErrUnexpectedError
+ Convey("wrong mediatype should fail validation", func() {
+ // create a manifest
+ manifest := ispec.Manifest{
+ MediaType: "bad.mediatype",
+ Config: ispec.Descriptor{
+ MediaType: ispec.MediaTypeImageConfig,
+ Size: int64(len(configBlob)),
+ Digest: configDigest,
+ },
+ Layers: []ispec.Descriptor{
+ {
+ MediaType: "application/vnd.oci.image.layer.v1.tar",
+ Digest: digest,
+ Size: int64(len(content)),
},
- }
-
- // call endpoint with session (added to client after previous request)
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
- })
-
- Convey("trigger session middle error GetUserGroups not found", func() {
- cookies := resp.Cookies()
-
- client.SetCookies(cookies)
+ },
+ Annotations: map[string]string{
+ "key": "val",
+ },
+ }
+ manifest.SchemaVersion = 2
- ctlr.MetaDB = mocks.MetaDBMock{
- GetUserGroupsFn: func(ctx context.Context) ([]string, error) {
- return []string{}, errors.ErrUserDataNotFound
- },
- }
+ mcontent, err := json.Marshal(manifest)
+ So(err, ShouldBeNil)
- // call endpoint with session (added to client after previous request)
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- })
+ resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
})
- Convey("trigger no email error in routes(callback)", func() {
- user := mockoidc.DefaultUser()
- user.Email = ""
-
- mockOIDCServer.QueueUser(user)
-
- client := resty.New()
- client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+ Convey("multiarch image should pass validation", func() {
+ index := ispec.Index{
+ MediaType: ispec.MediaTypeImageIndex,
+ Manifests: []ispec.Descriptor{
+ {
+ MediaType: ispec.MediaTypeImageManifest,
+ Digest: digest,
+ Size: int64(len((content))),
+ },
+ },
+ }
- client.SetCookie(&http.Cookie{Name: "session"})
+ index.SchemaVersion = 2
- // call endpoint with session (added to client after previous request)
- resp, err := client.R().
- SetQueryParam("provider", "oidc").
- Get(baseURL + constants.LoginPath)
+ indexContent, err := json.Marshal(index)
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- })
- Convey("trigger session save error in routes(callback)", func() {
- err := os.Chmod(rootDir, 0o000)
+ resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ SetBody(indexContent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/index", repoName))
So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ })
- defer func() {
- err := os.Chmod(rootDir, storageConstants.DefaultDirPerms)
- So(err, ShouldBeNil)
- }()
-
- client := resty.New()
- client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+ Convey("multiarch image without schemaVersion should fail validation", func() {
+ index := ispec.Index{
+ MediaType: ispec.MediaTypeImageIndex,
+ Manifests: []ispec.Descriptor{
+ {
+ MediaType: ispec.MediaTypeImageManifest,
+ Digest: digest,
+ Size: int64(len((content))),
+ },
+ },
+ }
- // first login user
- resp, err := client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- SetQueryParam("provider", "oidc").
- Get(baseURL + constants.LoginPath)
+ indexContent, err := json.Marshal(index)
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
- })
- Convey("trigger session save error in basicAuthn", func() {
- err := os.Chmod(rootDir, 0o000)
+ resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ SetBody(indexContent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/index", repoName))
So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+ })
- defer func() {
- err := os.Chmod(rootDir, storageConstants.DefaultDirPerms)
- So(err, ShouldBeNil)
- }()
+ Convey("multiarch image with missing manifest should fail validation", func() {
+ index := ispec.Index{
+ MediaType: ispec.MediaTypeImageIndex,
+ Manifests: []ispec.Descriptor{
+ {
+ MediaType: ispec.MediaTypeImageManifest,
+ Digest: digest,
+ Size: int64(len((content))),
+ },
+ {
+ MediaType: ispec.MediaTypeImageManifest,
+ Digest: godigest.FromString("missing layer"),
+ Size: 10,
+ },
+ },
+ }
- client := resty.New()
- client.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
+ index.SchemaVersion = 2
- // first htpasswd saveSessionLoggedUser() error
- resp, err := client.R().
- SetBasicAuth(htpasswdUsername, passphrase).
- Get(baseURL + "/v2/")
+ indexContent, err := json.Marshal(index)
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
- // second ldap saveSessionLoggedUser() error
- resp, err = client.R().
- SetBasicAuth(username, passphrase).
- Get(baseURL + "/v2/")
+ resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ SetBody(indexContent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/index", repoName))
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
})
+ })
+}
- Convey("trigger session middle errors", func() {
- client := resty.New()
- client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
-
- user := mockoidc.DefaultUser()
- user.Groups = []string{"group1", "group2"}
+func TestArtifactReferences(t *testing.T) {
+ Convey("Validate Artifact References", t, func() {
+ // start a new server
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
- mockOIDCServer.QueueUser(user)
+ conf := config.New()
+ conf.HTTP.Port = port
- // first login user
- resp, err := client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- SetQueryParam("provider", "oidc").
- Get(baseURL + constants.LoginPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
+ dir := t.TempDir()
+ ctlr := makeController(conf, dir)
+ cm := test.NewControllerManager(ctlr)
+ // this blocks
+ cm.StartServer()
+ time.Sleep(1000 * time.Millisecond)
+ defer cm.StopServer()
+
+ repoName := "artifact-repo"
+ content := []byte("this is a blob")
+ digest := godigest.FromBytes(content)
+ So(digest, ShouldNotBeNil)
+
+ cfg, layers, manifest, err := deprecated.GetImageComponents(2) //nolint:staticcheck
+ So(err, ShouldBeNil)
+
+ err = UploadImage(
+ Image{
+ Config: cfg,
+ Layers: layers,
+ Manifest: manifest,
+ }, baseURL, repoName, "1.0")
+ So(err, ShouldBeNil)
+
+ content, err = json.Marshal(manifest)
+ So(err, ShouldBeNil)
+ digest = godigest.FromBytes(content)
+ So(digest, ShouldNotBeNil)
+
+ artifactType := "application/vnd.example.icecream.v1"
+
+ Convey("Validate Image Manifest Reference", func() {
+ resp, err := resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+
+ var referrers ispec.Index
+ err = json.Unmarshal(resp.Body(), &referrers)
+ So(err, ShouldBeNil)
+ So(referrers.Manifests, ShouldBeEmpty)
+
+ // now upload a reference
+
+ // upload image config blob
+ resp, err = resty.R().Post(baseURL + fmt.Sprintf("/v2/%s/blobs/uploads/", repoName))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
+ loc := test.Location(baseURL, resp)
+ cblob, cdigest := getEmptyImageConfig()
+
+ resp, err = resty.R().
+ SetContentLength(true).
+ 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.StatusCode(), ShouldEqual, http.StatusCreated)
- Convey("trigger bad session encoding error in authn", func() {
- cookies := resp.Cookies()
- for _, cookie := range cookies {
- if cookie.Name == "session" {
- cookie.Value = "badSessionValue"
- }
- }
+ // create a manifest
+ manifest := ispec.Manifest{
+ Config: ispec.Descriptor{
+ MediaType: artifactType,
+ Digest: cdigest,
+ Size: int64(len(cblob)),
+ },
+ Layers: []ispec.Descriptor{
+ {
+ MediaType: "application/vnd.oci.image.layer.v1.tar",
+ Digest: digest,
+ Size: int64(len(content)),
+ },
+ },
+ Subject: &ispec.Descriptor{
+ MediaType: ispec.MediaTypeImageManifest,
+ Digest: digest,
+ Size: int64(len(content)),
+ },
+ Annotations: map[string]string{
+ "key": "val",
+ },
+ }
+ manifest.SchemaVersion = 2
- client.SetCookies(cookies)
+ Convey("Using invalid content", func() {
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody([]byte("invalid data")).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
- // call endpoint with session (added to client after previous request)
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Get(baseURL + "/v2/_catalog")
+ // unknown repo will return status not found
+ resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", "unknown", digest.String()))
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- })
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- Convey("web request without cookies", func() {
- client.SetCookie(&http.Cookie{})
+ resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- // call endpoint with session (added to client after previous request)
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Get(baseURL + "/v2/_catalog")
+ resp, err = resty.R().SetQueryParams(map[string]string{"artifactType": artifactType}).
+ Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- })
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- Convey("web request with userless cookie", func() {
- // first get session
- session, err := ctlr.CookieStore.Get(resp.RawResponse.Request, "session")
+ // create a bad manifest (constructed manually)
+ content := `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:71dbae9d7e6445fb5e0b11328e941b8e8937fdd52465079f536ce44bb78796ed","size":406}}` //nolint: lll
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody(content).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
- session.ID = invalidSessionID
- session.IsNew = false
- session.Values["authStatus"] = true
+ // missing layers
+ mcontent := []byte("this is a missing blob")
+ digest = godigest.FromBytes(mcontent)
+ So(digest, ShouldNotBeNil)
- cookieStore, ok := ctlr.CookieStore.(*sessions.FilesystemStore)
- So(ok, ShouldBeTrue)
+ manifest.Layers = append(manifest.Layers, ispec.Descriptor{
+ MediaType: "application/vnd.oci.image.layer.v1.tar",
+ Digest: digest,
+ Size: int64(len(mcontent)),
+ })
+
+ mcontent, err = json.Marshal(manifest)
+ So(err, ShouldBeNil)
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- // first encode sessionID
- encoded, err := securecookie.EncodeMulti(session.Name(), session.ID,
- cookieStore.Codecs...)
+ // invalid schema version
+ manifest.SchemaVersion = 1
+
+ mcontent, err = json.Marshal(manifest)
So(err, ShouldBeNil)
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
- // save cookie
- cookie := sessions.NewCookie(session.Name(), encoded, session.Options)
- client.SetCookie(cookie)
+ // upload image config blob
+ resp, err = resty.R().Post(baseURL + fmt.Sprintf("/v2/%s/blobs/uploads/", repoName))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
+ loc := test.Location(baseURL, resp)
+ cblob = []byte("{}")
+ cdigest = godigest.FromBytes(cblob)
+ So(cdigest, ShouldNotBeNil)
- // encode session values and save on disk
- encoded, err = securecookie.EncodeMulti(session.Name(), session.Values,
- cookieStore.Codecs...)
+ resp, err = resty.R().
+ SetContentLength(true).
+ 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.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ manifest := ispec.Manifest{
+ Config: ispec.Descriptor{
+ MediaType: "application/vnd.oci.image.config.v1+json",
+ Digest: cdigest,
+ Size: int64(len(cblob)),
+ },
+ }
- filename := filepath.Join(rootDir, "_sessions", "session_"+session.ID)
+ manifest.SchemaVersion = 2
+ mcontent, err = json.Marshal(manifest)
+ So(err, ShouldBeNil)
+ digest = godigest.FromBytes(mcontent)
+ So(digest, ShouldNotBeNil)
- err = os.WriteFile(filename, []byte(encoded), 0o600)
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+
+ // missing layers
+ mcontent = []byte("this is a missing blob")
+ digest = godigest.FromBytes(mcontent)
+ So(digest, ShouldNotBeNil)
+
+ manifest.Layers = append(manifest.Layers, ispec.Descriptor{
+ MediaType: "application/vnd.oci.image.layer.v1.tar",
+ Digest: digest,
+ Size: int64(len(mcontent)),
+ })
- // call endpoint with session (added to client after previous request)
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Get(baseURL + "/v2/_catalog")
+ mcontent, err = json.Marshal(manifest)
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- })
- Convey("web request with authStatus false cookie", func() {
- // first get session
- session, err := ctlr.CookieStore.Get(resp.RawResponse.Request, "session")
+ // should fail because config is of type image and blob is not uploaded
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+
+ // no layers at all
+ manifest.Layers = []ispec.Descriptor{}
- session.ID = invalidSessionID
- session.IsNew = false
- session.Values["authStatus"] = false
- session.Values["username"] = username
+ mcontent, err = json.Marshal(manifest)
+ So(err, ShouldBeNil)
- cookieStore, ok := ctlr.CookieStore.(*sessions.FilesystemStore)
- So(ok, ShouldBeTrue)
+ // should not fail
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ })
- // first encode sessionID
- encoded, err := securecookie.EncodeMulti(session.Name(), session.ID,
- cookieStore.Codecs...)
+ Convey("Using valid content", func() {
+ content, err = json.Marshal(manifest)
So(err, ShouldBeNil)
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody(content).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- // save cookie
- cookie := sessions.NewCookie(session.Name(), encoded, session.Options)
- client.SetCookie(cookie)
+ resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- // encode session values and save on disk
- encoded, err = securecookie.EncodeMulti(session.Name(), session.Values,
- cookieStore.Codecs...)
+ resp, err = resty.R().SetQueryParams(map[string]string{"artifact": "invalid"}).
+ Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- filename := filepath.Join(rootDir, "_sessions", "session_"+session.ID)
+ resp, err = resty.R().SetQueryParams(map[string]string{"artifactType": "invalid"}).
+ Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- err = os.WriteFile(filename, []byte(encoded), 0o600)
+ resp, err = resty.R().SetQueryParams(map[string]string{"artifactType": artifactType}).
+ Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ So(resp.Header().Get("Content-Type"), ShouldEqual, ispec.MediaTypeImageIndex)
+ So(resp.Header().Get("OCI-Filters-Applied"), ShouldEqual, "artifactType")
- // call endpoint with session (added to client after previous request)
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Get(baseURL + "/v2/_catalog")
+ resp, err = resty.R().SetQueryParams(map[string]string{"artifactType": artifactType +
+ ",otherArtType"}).Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName,
+ digest.String()))
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ So(resp.Header().Get("Content-Type"), ShouldEqual, ispec.MediaTypeImageIndex)
+ So(resp.Header().Get("OCI-Filters-Applied"), ShouldEqual, "artifactType")
})
})
})
}
-func TestAuthnMetaDBErrors(t *testing.T) {
- Convey("make controller", t, func() {
+//nolint:dupl // duplicated test code
+func TestRouteFailures(t *testing.T) {
+ Convey("Make a new controller", t, func() {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
- htpasswdPath := test.MakeHtpasswdFile()
- defer os.Remove(htpasswdPath)
+ ctlr := makeController(conf, t.TempDir())
+ ctlr.Config.Storage.Commit = true
- mockOIDCServer, err := authutils.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)
- }
- }()
+ rthdlr := api.NewRouteHandler(ctlr)
- rootDir := t.TempDir()
+ // NOTE: the url or method itself doesn't matter below since we are calling the handlers directly,
+ // so path routing is bypassed
- mockOIDCConfig := mockOIDCServer.Config()
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- OpenID: &config.OpenIDConfig{
- Providers: map[string]config.OpenIDProviderConfig{
- "oidc": {
- ClientID: mockOIDCConfig.ClientID,
- ClientSecret: mockOIDCConfig.ClientSecret,
- KeyPath: "",
- Issuer: mockOIDCConfig.Issuer,
- Scopes: []string{"openid", "email"},
- },
- },
- },
- }
+ Convey("List tags", func() {
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ mux.SetURLVars(request, map[string]string{})
+ response := httptest.NewRecorder()
- ctlr := api.NewController(conf)
+ rthdlr.ListTags(response, request)
- ctlr.Config.Storage.RootDirectory = rootDir
+ resp := response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- cm := test.NewControllerManager(ctlr)
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ response = httptest.NewRecorder()
- cm.StartServer()
- defer cm.StopServer()
- test.WaitTillServerReady(baseURL)
+ rthdlr.ListTags(response, request)
- Convey("trigger basic authn middle(htpasswd) error", func() {
- client := resty.New()
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- ctlr.MetaDB = mocks.MetaDBMock{
- SetUserGroupsFn: func(ctx context.Context, groups []string) error {
- return ErrUnexpectedError
- },
- }
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ qparm := request.URL.Query()
+ qparm.Add("n", "a")
+ request.URL.RawQuery = qparm.Encode()
+ response = httptest.NewRecorder()
- resp, err := client.R().
- SetBasicAuth(username, passphrase).
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
- })
+ rthdlr.ListTags(response, request)
- Convey("trigger session middle metaDB errors", func() {
- client := resty.New()
- client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
- user := mockoidc.DefaultUser()
- user.Groups = []string{"group1", "group2"}
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ qparm = request.URL.Query()
+ qparm.Add("n", "-1")
+ request.URL.RawQuery = qparm.Encode()
+ response = httptest.NewRecorder()
- mockOIDCServer.QueueUser(user)
+ rthdlr.ListTags(response, request)
- // first login user
- resp, err := client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- SetQueryParam("provider", "oidc").
- Get(baseURL + constants.LoginPath)
- So(err, ShouldBeNil)
+ resp = response.Result()
+ defer resp.Body.Close()
So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
- Convey("trigger session middle error", func() {
- cookies := resp.Cookies()
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ qparm = request.URL.Query()
+ qparm.Add("n", "abc")
+ request.URL.RawQuery = qparm.Encode()
+ response = httptest.NewRecorder()
- client.SetCookies(cookies)
+ rthdlr.ListTags(response, request)
- ctlr.MetaDB = mocks.MetaDBMock{
- GetUserGroupsFn: func(ctx context.Context) ([]string, error) {
- return []string{}, ErrUnexpectedError
- },
- }
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
- // call endpoint with session (added to client after previous request)
- resp, err = client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
- })
- })
- })
-}
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ qparm = request.URL.Query()
+ qparm.Add("n", "a")
+ qparm.Add("n", "abc")
+ request.URL.RawQuery = qparm.Encode()
+ response = httptest.NewRecorder()
-func TestAuthorization(t *testing.T) {
- Convey("Make a new controller", t, func() {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- conf := config.New()
- conf.HTTP.Port = port
- htpasswdPath := test.MakeHtpasswdFile()
- defer os.Remove(htpasswdPath)
+ rthdlr.ListTags(response, request)
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- }
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Repositories: config.Repositories{
- AuthorizationAllRepos: config.PolicyGroup{
- Policies: []config.Policy{
- {
- Users: []string{},
- Actions: []string{},
- },
- },
- DefaultPolicy: []string{},
- },
- },
- AdminPolicy: config.Policy{
- Users: []string{},
- Actions: []string{},
- },
- }
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
- Convey("with openid", func() {
- mockOIDCServer, err := authutils.MockOIDCRun()
- if err != nil {
- panic(err)
- }
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ qparm = request.URL.Query()
+ qparm.Add("n", "0")
+ request.URL.RawQuery = qparm.Encode()
+ response = httptest.NewRecorder()
- defer func() {
- err := mockOIDCServer.Shutdown()
- if err != nil {
- panic(err)
- }
- }()
-
- mockOIDCConfig := mockOIDCServer.Config()
- conf.HTTP.Auth = &config.AuthConfig{
- OpenID: &config.OpenIDConfig{
- Providers: map[string]config.OpenIDProviderConfig{
- "oidc": {
- ClientID: mockOIDCConfig.ClientID,
- ClientSecret: mockOIDCConfig.ClientSecret,
- KeyPath: "",
- Issuer: mockOIDCConfig.Issuer,
- Scopes: []string{"openid", "email"},
- },
- },
- },
- }
+ rthdlr.ListTags(response, request)
- ctlr := api.NewController(conf)
- ctlr.Config.Storage.RootDirectory = t.TempDir()
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- err = WriteImageToFileSystem(CreateDefaultImage(), "zot-test", "0.0.1",
- ociutils.GetDefaultStoreController(ctlr.Config.Storage.RootDirectory, ctlr.Log))
- So(err, ShouldBeNil)
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ qparm = request.URL.Query()
+ qparm.Add("n", "1")
+ qparm.Add("last", "")
+ request.URL.RawQuery = qparm.Encode()
+ response = httptest.NewRecorder()
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ rthdlr.ListTags(response, request)
- client := resty.New()
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- client.SetRedirectPolicy(test.CustomRedirectPolicy(20))
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ qparm = request.URL.Query()
+ qparm.Add("n", "1")
+ qparm.Add("last", "a")
+ request.URL.RawQuery = qparm.Encode()
+ response = httptest.NewRecorder()
- mockOIDCServer.QueueUser(&mockoidc.MockUser{
- Email: "test",
- Subject: "1234567890",
- })
+ rthdlr.ListTags(response, request)
- // first login user
- resp, err := client.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- SetQueryParam("provider", "oidc").
- Get(baseURL + constants.LoginPath)
- So(err, ShouldBeNil)
+ resp = response.Result()
+ defer resp.Body.Close()
So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
-
- client.SetCookies(resp.Cookies())
- client.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- RunAuthorizationTests(t, client, baseURL, conf)
- })
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ qparm = request.URL.Query()
+ qparm.Add("last", "a")
+ request.URL.RawQuery = qparm.Encode()
+ response = httptest.NewRecorder()
- Convey("with basic auth", func() {
- ctlr := api.NewController(conf)
- ctlr.Config.Storage.RootDirectory = t.TempDir()
+ rthdlr.ListTags(response, request)
- err := WriteImageToFileSystem(CreateDefaultImage(), "zot-test", "0.0.1",
- ociutils.GetDefaultStoreController(ctlr.Config.Storage.RootDirectory, ctlr.Log))
- So(err, ShouldBeNil)
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ qparm = request.URL.Query()
+ qparm.Add("n", "1")
+ qparm.Add("last", "a")
+ qparm.Add("last", "abc")
+ request.URL.RawQuery = qparm.Encode()
+ response = httptest.NewRecorder()
- client := resty.New()
- client.SetBasicAuth(username, passphrase)
+ rthdlr.ListTags(response, request)
- RunAuthorizationTests(t, client, baseURL, conf)
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
})
- })
-}
-
-func TestGetUsername(t *testing.T) {
- Convey("Make a new controller", t, func() {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- htpasswdPath := test.MakeHtpasswdFileFromString(getCredString(username, passphrase))
- defer os.Remove(htpasswdPath)
+ Convey("Check manifest", func() {
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodHead, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{})
+ response := httptest.NewRecorder()
- conf := config.New()
- conf.HTTP.Port = port
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- }
+ rthdlr.CheckManifest(response, request)
- dir := t.TempDir()
- ctlr := makeController(conf, dir)
+ resp := response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodHead, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ response = httptest.NewRecorder()
- resp, err := resty.R().Get(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ rthdlr.CheckManifest(response, request)
- // test base64 encode
- resp, err = resty.R().SetHeader("Authorization", "Basic should fail").Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- // test "username:password" encoding
- resp, err = resty.R().SetHeader("Authorization", "Basic dGVzdA==").Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodHead, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo", "reference": ""})
+ response = httptest.NewRecorder()
- // failed parsing authorization header
- resp, err = resty.R().SetHeader("Authorization", "Basic ").Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
+ rthdlr.CheckManifest(response, request)
- 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)
- })
-}
-
-func TestAuthorizationWithOnlyAnonymousPolicy(t *testing.T) {
- Convey("Make a new controller", t, func() {
- const TestRepo = "my-repos/repo"
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
-
- conf := config.New()
- conf.HTTP.Port = port
- conf.HTTP.Auth = &config.AuthConfig{}
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Repositories: config.Repositories{
- TestRepo: config.PolicyGroup{
- AnonymousPolicy: []string{"read"},
- },
- },
- }
-
- dir := t.TempDir()
- ctlr := makeController(conf, dir)
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
-
- blob := []byte("hello, blob!")
- digest := godigest.FromBytes(blob).String()
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
+ })
- resp, err := resty.R().Get(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ Convey("Get manifest", func() {
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{})
+ response := httptest.NewRecorder()
- resp, err = resty.R().Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- var e apiErr.Error
- err = json.Unmarshal(resp.Body(), &e)
- So(err, ShouldBeNil)
+ rthdlr.GetManifest(response, request)
- // 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.StatusUnauthorized)
+ resp := response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- if entry, ok := conf.HTTP.AccessControl.Repositories[TestRepo]; ok {
- entry.AnonymousPolicy = []string{"create", "read"}
- conf.HTTP.AccessControl.Repositories[TestRepo] = entry
- }
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ response = httptest.NewRecorder()
- // now it should get 202
- resp, err = resty.R().Post(baseURL + "/v2/" + TestRepo + "/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
- loc := resp.Header().Get("Location")
+ rthdlr.GetManifest(response, request)
- // uploading blob should get 201
- resp, err = resty.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)
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- cblob, cdigest := GetRandomImageConfig()
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo", "reference": ""})
+ response = httptest.NewRecorder()
- resp, err = resty.R().Post(baseURL + "/v2/" + TestRepo + "/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
- loc = test.Location(baseURL, resp)
+ rthdlr.GetManifest(response, request)
- // uploading blob should get 201
- resp, err = resty.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)
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
+ })
- manifest := 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(blob),
- Size: int64(len(blob)),
- },
- },
- }
- manifest.SchemaVersion = 2
- manifestBlob, err := json.Marshal(manifest)
- So(err, ShouldBeNil)
+ Convey("Update manifest", func() {
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{})
+ response := httptest.NewRecorder()
- resp, err = resty.R().
- SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
- SetBody(manifestBlob).
- Put(baseURL + "/v2/" + TestRepo + "/manifests/0.0.2")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ rthdlr.UpdateManifest(response, request)
- updateBlob := []byte("Hello, blob update!")
+ resp := response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- resp, err = resty.R().
- Post(baseURL + "/v2/" + TestRepo + "/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().
- 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)
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ response = httptest.NewRecorder()
- 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)
+ rthdlr.UpdateManifest(response, request)
- // 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.StatusUnauthorized)
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- // get the manifest and check if it's the old one
- resp, err = resty.R().
- Get(baseURL + "/v2/" + TestRepo + "/manifests/0.0.2")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- So(resp.Body(), ShouldResemble, manifestBlob)
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo", "reference": ""})
+ response = httptest.NewRecorder()
- // add update perm on repo
- if entry, ok := conf.HTTP.AccessControl.Repositories[TestRepo]; ok {
- entry.AnonymousPolicy = []string{"create", "read", "update"}
- conf.HTTP.AccessControl.Repositories[TestRepo] = entry
- }
+ rthdlr.UpdateManifest(response, request)
- // update manifest should get 201 with update perm
- resp, err = resty.R().
- SetHeader("Content-type", "application/vnd.oci.image.manifest.v1+json").
- SetBody(updatedManifestBlob).
- Put(baseURL + "/v2/" + TestRepo + "/manifests/0.0.2")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
+ })
- // get the manifest and check if it's the new updated one
- resp, err = resty.R().
- Get(baseURL + "/v2/" + TestRepo + "/manifests/0.0.2")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- So(resp.Body(), ShouldResemble, updatedManifestBlob)
+ Convey("Delete manifest", func() {
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{})
+ response := httptest.NewRecorder()
- resp, err = resty.R().Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ rthdlr.DeleteManifest(response, request)
- // make sure anonymous is correctly handled when using acCtx (requestcontext package)
- catalog := struct {
- Repositories []string `json:"repositories"`
- }{}
+ resp := response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- err = json.Unmarshal(resp.Body(), &catalog)
- So(err, ShouldBeNil)
- So(len(catalog.Repositories), ShouldEqual, 1)
- So(catalog.Repositories, ShouldContain, TestRepo)
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ response = httptest.NewRecorder()
- err = os.Mkdir(path.Join(dir, "zot-test"), storageConstants.DefaultDirPerms)
- So(err, ShouldBeNil)
+ rthdlr.DeleteManifest(response, request)
- err = WriteImageToFileSystem(CreateDefaultImage(), "zot-test", "tag", ctlr.StoreController)
- So(err, ShouldBeNil)
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- // 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)
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo", "reference": ""})
+ response = httptest.NewRecorder()
- err = json.Unmarshal(resp.Body(), &catalog)
- So(err, ShouldBeNil)
- So(len(catalog.Repositories), ShouldEqual, 1)
- So(catalog.Repositories, ShouldContain, TestRepo)
+ rthdlr.DeleteManifest(response, request)
- // add rights
- conf.HTTP.AccessControl.Repositories["zot-test"] = config.PolicyGroup{
- AnonymousPolicy: []string{"read"},
- }
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
+ })
- resp, err = resty.R().Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ Convey("Check blob", func() {
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodHead, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{})
+ response := httptest.NewRecorder()
- 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")
- })
-}
+ rthdlr.CheckBlob(response, request)
-func TestAuthorizationWithAnonymousPolicyBasicAuthAndSessionHeader(t *testing.T) {
- Convey("Make a new controller", t, func() {
- const TestRepo = "my-repos/repo"
- const AllRepos = "**"
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
+ resp := response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- badpassphrase := "bad"
- htpasswdContent := fmt.Sprintf("%s:$2y$05$hlbSXDp6hzDLu6VwACS39ORvVRpr3OMR4RlJ31jtlaOEGnPjKZI1m\n",
- htpasswdUsername)
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodHead, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ response = httptest.NewRecorder()
- htpasswdPath := test.MakeHtpasswdFileFromString(htpasswdContent)
- defer os.Remove(htpasswdPath)
+ rthdlr.CheckBlob(response, request)
- img := CreateRandomImage()
- tagAnonymous := "1.0-anon"
- tagAuth := "1.0-auth"
- tagUnauth := "1.0-unauth"
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- conf := config.New()
- conf.HTTP.Port = port
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- }
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Repositories: config.Repositories{
- AllRepos: config.PolicyGroup{
- Policies: []config.Policy{
- {
- Users: []string{htpasswdUsername},
- Actions: []string{"read"},
- },
- },
- AnonymousPolicy: []string{"read"},
- },
- },
- }
-
- dir := t.TempDir()
- ctlr := makeController(conf, dir)
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
-
- // /v2 access
- // Can access /v2 without credentials
- resp, err := resty.R().Get(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- // Can access /v2 without credentials and with X-Zot-Api-Client=zot-ui
- resp, err = resty.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Get(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- // Can access /v2 with correct credentials
- resp, err = resty.R().
- SetBasicAuth(htpasswdUsername, passphrase).
- Get(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- // Fail to access /v2 with incorrect credentials
- resp, err = resty.R().
- SetBasicAuth(htpasswdUsername, badpassphrase).
- Get(baseURL + "/v2/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
-
- // Catalog access
- resp, err = resty.R().Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- var apiError apiErr.Error
- err = json.Unmarshal(resp.Body(), &apiError)
- So(err, ShouldBeNil)
-
- resp, err = resty.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- apiError = apiErr.Error{}
- err = json.Unmarshal(resp.Body(), &apiError)
- So(err, ShouldBeNil)
-
- resp, err = resty.R().
- SetBasicAuth(htpasswdUsername, passphrase).
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- apiError = apiErr.Error{}
- err = json.Unmarshal(resp.Body(), &apiError)
- So(err, ShouldBeNil)
-
- resp, err = resty.R().
- SetBasicAuth(htpasswdUsername, badpassphrase).
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- apiError = apiErr.Error{}
- err = json.Unmarshal(resp.Body(), &apiError)
- So(err, ShouldBeNil)
-
- // upload capability
- // should get 403 without create
- err = UploadImage(img, baseURL, TestRepo, tagAnonymous)
- So(err, ShouldNotBeNil)
-
- err = UploadImageWithBasicAuth(img, baseURL,
- TestRepo, tagAuth, htpasswdUsername, passphrase)
- So(err, ShouldNotBeNil)
-
- err = UploadImageWithBasicAuth(img, baseURL,
- TestRepo, tagUnauth, htpasswdUsername, badpassphrase)
- So(err, ShouldNotBeNil)
-
- if entry, ok := conf.HTTP.AccessControl.Repositories[AllRepos]; ok {
- entry.AnonymousPolicy = []string{"create", "read"}
- entry.Policies[0] = config.Policy{
- Users: []string{htpasswdUsername},
- Actions: []string{"create", "read"},
- }
- conf.HTTP.AccessControl.Repositories[AllRepos] = entry
- }
-
- // now it should succeed for valid users
- err = UploadImage(img, baseURL, TestRepo, tagAnonymous)
- So(err, ShouldBeNil)
-
- err = UploadImageWithBasicAuth(img, baseURL,
- TestRepo, tagAuth, htpasswdUsername, passphrase)
- So(err, ShouldBeNil)
-
- err = UploadImageWithBasicAuth(img, baseURL,
- TestRepo, tagUnauth, htpasswdUsername, badpassphrase)
- So(err, ShouldNotBeNil)
-
- // read capability
- catalog := struct {
- Repositories []string `json:"repositories"`
- }{}
-
- 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)
-
- catalog = struct {
- Repositories []string `json:"repositories"`
- }{}
-
- resp, err = resty.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- 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)
-
- catalog = struct {
- Repositories []string `json:"repositories"`
- }{}
-
- resp, err = resty.R().
- SetBasicAuth(htpasswdUsername, passphrase).
- 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)
-
- resp, err = resty.R().
- SetBasicAuth(htpasswdUsername, badpassphrase).
- Get(baseURL + "/v2/_catalog")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- })
-}
-
-func TestAuthorizationWithMultiplePolicies(t *testing.T) {
- Convey("Make a new controller", t, func() {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
-
- conf := config.New()
- conf.HTTP.Port = port
- // have two users: "test" user for user Policy, and "bob" for default policy
- htpasswdPath := test.MakeHtpasswdFileFromString(getCredString(username, passphrase) +
- "\n" + getCredString("bob", passphrase))
- defer os.Remove(htpasswdPath)
-
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- }
- // config with all policy types, to test that the correct one is applied in each case
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Repositories: config.Repositories{
- AuthorizationAllRepos: config.PolicyGroup{
- Policies: []config.Policy{
- {
- Users: []string{},
- Actions: []string{},
- },
- },
- DefaultPolicy: []string{},
- AnonymousPolicy: []string{},
- },
- },
- AdminPolicy: config.Policy{
- Users: []string{},
- Actions: []string{},
- },
- }
-
- Convey("with openid", func() {
- dir := t.TempDir()
-
- mockOIDCServer, err := authutils.MockOIDCRun()
- if err != nil {
- panic(err)
- }
-
- defer func() {
- err := mockOIDCServer.Shutdown()
- if err != nil {
- panic(err)
- }
- }()
-
- mockOIDCConfig := mockOIDCServer.Config()
- conf.HTTP.Auth = &config.AuthConfig{
- OpenID: &config.OpenIDConfig{
- Providers: map[string]config.OpenIDProviderConfig{
- "oidc": {
- ClientID: mockOIDCConfig.ClientID,
- ClientSecret: mockOIDCConfig.ClientSecret,
- KeyPath: "",
- Issuer: mockOIDCConfig.Issuer,
- Scopes: []string{"openid", "email"},
- },
- },
- },
- }
-
- ctlr := api.NewController(conf)
- ctlr.Config.Storage.RootDirectory = dir
-
- err = WriteImageToFileSystem(CreateDefaultImage(), "zot-test", "0.0.1",
- ociutils.GetDefaultStoreController(ctlr.Config.Storage.RootDirectory, ctlr.Log))
- So(err, ShouldBeNil)
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
-
- testUserClient := resty.New()
-
- testUserClient.SetRedirectPolicy(test.CustomRedirectPolicy(20))
-
- mockOIDCServer.QueueUser(&mockoidc.MockUser{
- Email: "test",
- Subject: "1234567890",
- })
-
- // first login user
- resp, err := testUserClient.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- SetQueryParam("provider", "oidc").
- Get(baseURL + constants.LoginPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
-
- testUserClient.SetCookies(resp.Cookies())
- testUserClient.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
-
- bobUserClient := resty.New()
-
- bobUserClient.SetRedirectPolicy(test.CustomRedirectPolicy(20))
-
- mockOIDCServer.QueueUser(&mockoidc.MockUser{
- Email: "bob",
- Subject: "1234567890",
- })
-
- // first login user
- resp, err = bobUserClient.R().
- SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue).
- SetQueryParam("provider", "oidc").
- Get(baseURL + constants.LoginPath)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
-
- bobUserClient.SetCookies(resp.Cookies())
- bobUserClient.SetHeader(constants.SessionClientHeaderName, constants.SessionClientHeaderValue)
-
- RunAuthorizationWithMultiplePoliciesTests(t, testUserClient, bobUserClient, baseURL, conf)
- })
-
- Convey("with basic auth", func() {
- dir := t.TempDir()
-
- ctlr := api.NewController(conf)
- ctlr.Config.Storage.RootDirectory = dir
-
- err := WriteImageToFileSystem(CreateDefaultImage(), "zot-test", "0.0.1",
- ociutils.GetDefaultStoreController(ctlr.Config.Storage.RootDirectory, ctlr.Log))
- So(err, ShouldBeNil)
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
-
- testUserClient := resty.New()
- testUserClient.SetBasicAuth(username, passphrase)
-
- bobUserClient := resty.New()
- bobUserClient.SetBasicAuth("bob", passphrase)
-
- RunAuthorizationWithMultiplePoliciesTests(t, testUserClient, bobUserClient, baseURL, conf)
- })
- })
-}
-
-func TestInvalidCases(t *testing.T) {
- Convey("Invalid repo dir", t, func() {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
-
- conf := config.New()
- conf.HTTP.Port = port
- htpasswdPath := test.MakeHtpasswdFileFromString(getCredString(username, passphrase))
-
- defer os.Remove(htpasswdPath)
-
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- }
-
- dir := t.TempDir()
- ctlr := makeController(conf, dir)
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer func(ctrl *api.Controller) {
- err := os.Chmod(dir, 0o755)
- if err != nil {
- panic(err)
- }
-
- err = ctrl.Server.Shutdown(context.Background())
- if err != nil {
- panic(err)
- }
-
- err = os.RemoveAll(ctrl.Config.Storage.RootDirectory)
- if err != nil {
- panic(err)
- }
- }(ctlr)
-
- err := os.Chmod(dir, 0o000)
- if err != nil {
- panic(err)
- }
-
- digest := godigest.FromString("dummy").String()
- name := "zot-c-test"
-
- client := resty.New()
-
- params := make(map[string]string)
- params["from"] = "zot-cveid-test"
- params["mount"] = digest
-
- postResponse, err := client.R().
- SetBasicAuth(username, passphrase).SetQueryParams(params).
- Post(fmt.Sprintf("%s/v2/%s/blobs/uploads/", baseURL, name))
- So(err, ShouldBeNil)
- So(postResponse.StatusCode(), ShouldEqual, http.StatusInternalServerError)
- })
-}
-
-func TestHTTPReadOnly(t *testing.T) {
- Convey("Single cred", t, func() {
- singleCredtests := []string{}
- user := ALICE
- password := ALICE
- singleCredtests = append(singleCredtests, getCredString(user, password))
- singleCredtests = append(singleCredtests, getCredString(user, password)+"\n")
-
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
-
- for _, testString := range singleCredtests {
- func() {
- conf := config.New()
- conf.HTTP.Port = port
- // enable read-only mode
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Repositories: config.Repositories{
- AuthorizationAllRepos: config.PolicyGroup{
- DefaultPolicy: []string{"read"},
- },
- },
- }
-
- htpasswdPath := test.MakeHtpasswdFileFromString(testString)
- defer os.Remove(htpasswdPath)
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- }
- ctlr := makeController(conf, t.TempDir())
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
-
- // with creds, should get expected status code
- resp, _ := resty.R().SetBasicAuth(user, password).Get(baseURL + "/v2/")
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- // with creds, any modifications should still fail on read-only mode
- resp, err := resty.R().SetBasicAuth(user, password).
- Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
-
- // with invalid creds, it should fail
- resp, _ = resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/")
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized)
- }()
- }
- })
-}
-
-func TestCrossRepoMount(t *testing.T) {
- Convey("Cross Repo Mount", t, func() {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
-
- conf := config.New()
- conf.HTTP.Port = port
- htpasswdPath := test.MakeHtpasswdFileFromString(getCredString(username, passphrase))
-
- defer os.Remove(htpasswdPath)
-
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- }
-
- dir := t.TempDir()
- ctlr := api.NewController(conf)
-
- ctlr.Config.Storage.RootDirectory = dir
- ctlr.Config.Storage.RemoteCache = false
- ctlr.Config.Storage.Dedupe = false
-
- image := CreateDefaultImage()
- err := WriteImageToFileSystem(image, "zot-cve-test", "test", storage.StoreController{
- DefaultStore: ociutils.GetDefaultImageStore(dir, ctlr.Log),
- })
- So(err, ShouldBeNil)
-
- cm := test.NewControllerManager(ctlr) //nolint: varnamelen
- cm.StartAndWait(port)
-
- params := make(map[string]string)
-
- manifestDigest := image.ManifestDescriptor.Digest
-
- dgst := manifestDigest
- name := "zot-cve-test"
- params["mount"] = string(manifestDigest)
- params["from"] = name
-
- client := resty.New()
- headResponse, err := client.R().SetBasicAuth(username, passphrase).
- Head(fmt.Sprintf("%s/v2/%s/blobs/%s", baseURL, name, manifestDigest))
- So(err, ShouldBeNil)
- So(headResponse.StatusCode(), ShouldEqual, http.StatusOK)
-
- // All invalid request of mount should return 202.
- params["mount"] = "sha:"
-
- postResponse, err := client.R().
- SetBasicAuth(username, passphrase).SetQueryParams(params).
- Post(baseURL + "/v2/zot-c-test/blobs/uploads/")
- So(err, ShouldBeNil)
- So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
- location, err := postResponse.RawResponse.Location()
- So(err, ShouldBeNil)
- So(location.String(), ShouldStartWith, fmt.Sprintf("%s%s/zot-c-test/%s/%s",
- baseURL, constants.RoutePrefix, constants.Blobs, constants.Uploads))
-
- incorrectParams := make(map[string]string)
- incorrectParams["mount"] = godigest.FromString("dummy").String()
- incorrectParams["from"] = "zot-x-test"
-
- postResponse, err = client.R().
- SetBasicAuth(username, passphrase).SetQueryParams(incorrectParams).
- Post(baseURL + "/v2/zot-y-test/blobs/uploads/")
- So(err, ShouldBeNil)
- So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
- So(test.Location(baseURL, postResponse), ShouldStartWith, fmt.Sprintf("%s%s/zot-y-test/%s/%s",
- baseURL, constants.RoutePrefix, constants.Blobs, constants.Uploads))
-
- // Use correct request
- // This is correct request but it will return 202 because blob is not present in cache.
- params["mount"] = string(manifestDigest)
- postResponse, err = client.R().
- SetBasicAuth(username, passphrase).SetQueryParams(params).
- Post(baseURL + "/v2/zot-c-test/blobs/uploads/")
- So(err, ShouldBeNil)
- So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
- So(test.Location(baseURL, postResponse), ShouldStartWith, fmt.Sprintf("%s%s/zot-c-test/%s/%s",
- baseURL, constants.RoutePrefix, constants.Blobs, constants.Uploads))
-
- // Send same request again
- postResponse, err = client.R().
- SetBasicAuth(username, passphrase).SetQueryParams(params).
- Post(baseURL + "/v2/zot-c-test/blobs/uploads/")
- So(err, ShouldBeNil)
- So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
-
- // Valid requests
- postResponse, err = client.R().
- SetBasicAuth(username, passphrase).SetQueryParams(params).
- Post(baseURL + "/v2/zot-d-test/blobs/uploads/")
- So(err, ShouldBeNil)
- So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
-
- headResponse, err = client.R().SetBasicAuth(username, passphrase).
- Head(fmt.Sprintf("%s/v2/zot-cv-test/blobs/%s", baseURL, manifestDigest))
- So(err, ShouldBeNil)
- So(headResponse.StatusCode(), ShouldEqual, http.StatusNotFound)
-
- postResponse, err = client.R().
- SetBasicAuth(username, passphrase).SetQueryParams(params).Post(baseURL + "/v2/zot-c-test/blobs/uploads/")
- So(err, ShouldBeNil)
- So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
-
- postResponse, err = client.R().
- SetBasicAuth(username, passphrase).SetQueryParams(params).
- Post(baseURL + "/v2/ /blobs/uploads/")
- So(err, ShouldBeNil)
- So(postResponse.StatusCode(), ShouldEqual, http.StatusNotFound)
-
- blob := manifestDigest.Encoded()
-
- buf, err := os.ReadFile(path.Join(ctlr.Config.Storage.RootDirectory, "zot-cve-test/blobs/sha256/"+blob))
- if err != nil {
- panic(err)
- }
-
- postResponse, err = client.R().SetHeader("Content-type", "application/octet-stream").
- SetBasicAuth(username, passphrase).SetQueryParam("digest", "sha256:"+blob).
- SetBody(buf).Post(baseURL + "/v2/zot-d-test/blobs/uploads/")
- So(err, ShouldBeNil)
- So(postResponse.StatusCode(), ShouldEqual, http.StatusCreated)
-
- // We have uploaded a blob and since we have provided digest it should be full blob upload and there should be entry
- // in cache, now try mount blob request status and it should be 201 because now blob is present in cache
- // and it should do hard link.
-
- // make a new server with dedupe on and same rootDir (can't restart because of metadb - boltdb being open)
- newDir := t.TempDir()
- err = test.CopyFiles(dir, newDir)
- So(err, ShouldBeNil)
-
- cm.StopServer()
-
- ctlr.Config.Storage.Dedupe = true
- ctlr.Config.Storage.GC = false
- ctlr.Config.Storage.RootDirectory = newDir
- cm = test.NewControllerManager(ctlr) //nolint: varnamelen
- cm.StartAndWait(port)
- defer cm.StopServer()
-
- // wait for dedupe task to run
- time.Sleep(10 * time.Second)
-
- params["mount"] = string(manifestDigest)
- postResponse, err = client.R().
- SetBasicAuth(username, passphrase).SetQueryParams(params).
- Post(baseURL + "/v2/zot-mount-test/blobs/uploads/")
- So(err, ShouldBeNil)
- So(postResponse.StatusCode(), ShouldEqual, http.StatusCreated)
- So(test.Location(baseURL, postResponse), ShouldEqual, fmt.Sprintf("%s%s/zot-mount-test/%s/%s:%s",
- baseURL, constants.RoutePrefix, constants.Blobs, godigest.SHA256, blob))
-
- // Check os.SameFile here
- cachePath := path.Join(ctlr.Config.Storage.RootDirectory, "zot-d-test", "blobs/sha256", dgst.Encoded())
-
- cacheFi, err := os.Stat(cachePath)
- So(err, ShouldBeNil)
-
- linkPath := path.Join(ctlr.Config.Storage.RootDirectory, "zot-mount-test", "blobs/sha256", dgst.Encoded())
-
- linkFi, err := os.Stat(linkPath)
- So(err, ShouldBeNil)
-
- So(os.SameFile(cacheFi, linkFi), ShouldEqual, true)
-
- // Now try another mount request and this time it should be from above uploaded repo i.e zot-mount-test
- // mount request should pass and should return 201.
- params["mount"] = string(manifestDigest)
- params["from"] = "zot-mount-test"
- postResponse, err = client.R().
- SetBasicAuth(username, passphrase).SetQueryParams(params).
- Post(baseURL + "/v2/zot-mount1-test/blobs/uploads/")
- So(err, ShouldBeNil)
- So(postResponse.StatusCode(), ShouldEqual, http.StatusCreated)
- So(test.Location(baseURL, postResponse), ShouldEqual, fmt.Sprintf("%s%s/zot-mount1-test/%s/%s:%s",
- baseURL, constants.RoutePrefix, constants.Blobs, godigest.SHA256, blob))
-
- linkPath = path.Join(ctlr.Config.Storage.RootDirectory, "zot-mount1-test", "blobs/sha256", dgst.Encoded())
-
- linkFi, err = os.Stat(linkPath)
- So(err, ShouldBeNil)
-
- So(os.SameFile(cacheFi, linkFi), ShouldEqual, true)
-
- headResponse, err = client.R().SetBasicAuth(username, passphrase).
- Head(fmt.Sprintf("%s/v2/zot-cv-test/blobs/%s", baseURL, manifestDigest))
- So(err, ShouldBeNil)
- So(headResponse.StatusCode(), ShouldEqual, http.StatusOK)
-
- // Invalid request
- params = make(map[string]string)
- params["mount"] = "sha256:"
- postResponse, err = client.R().
- SetBasicAuth(username, passphrase).SetQueryParams(params).
- Post(baseURL + "/v2/zot-mount-test/blobs/uploads/")
- So(err, ShouldBeNil)
- So(postResponse.StatusCode(), ShouldEqual, http.StatusAccepted)
-
- params = make(map[string]string)
- params["from"] = "zot-cve-test"
- postResponse, err = client.R().
- SetBasicAuth(username, passphrase).SetQueryParams(params).
- Post(baseURL + "/v2/zot-mount-test/blobs/uploads/")
- So(err, ShouldBeNil)
- So(postResponse.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
- })
-
- Convey("Disable dedupe and cache", t, func() {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
-
- conf := config.New()
- conf.HTTP.Port = port
- htpasswdPath := test.MakeHtpasswdFileFromString(getCredString(username, passphrase))
-
- defer os.Remove(htpasswdPath)
-
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- }
-
- dir := t.TempDir()
-
- ctlr := api.NewController(conf)
- ctlr.Config.Storage.RootDirectory = dir
- ctlr.Config.Storage.Dedupe = false
- ctlr.Config.Storage.GC = false
-
- image := CreateImageWith().RandomLayers(1, 10).DefaultConfig().Build()
-
- err := WriteImageToFileSystem(image, "zot-cve-test", "0.0.1",
- ociutils.GetDefaultStoreController(dir, ctlr.Log))
- So(err, ShouldBeNil)
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
-
- // digest := test.GetTestBlobDigest("zot-cve-test", "layer").String()
- digest := godigest.FromBytes(image.Layers[0])
- name := "zot-c-test"
- client := resty.New()
- headResponse, err := client.R().SetBasicAuth(username, passphrase).
- Head(fmt.Sprintf("%s/v2/%s/blobs/%s", baseURL, name, digest))
- So(err, ShouldBeNil)
- So(headResponse.StatusCode(), ShouldEqual, http.StatusNotFound)
- })
-}
-
-func TestParallelRequests(t *testing.T) {
- t.Parallel()
-
- testCases := []struct {
- srcImageName string
- srcImageTag string
- destImageName string
- destImageTag string
- testCaseName string
- }{
- {
- srcImageName: "zot-test",
- srcImageTag: "0.0.1",
- destImageName: "zot-1-test",
- destImageTag: "0.0.1",
- testCaseName: "Request-1",
- },
- {
- srcImageName: "zot-test",
- srcImageTag: "0.0.1",
- destImageName: "zot-2-test",
- testCaseName: "Request-2",
- },
- {
- srcImageName: "zot-cve-test",
- srcImageTag: "0.0.1",
- destImageName: "a/zot-3-test",
- testCaseName: "Request-3",
- },
- {
- srcImageName: "zot-cve-test",
- srcImageTag: "0.0.1",
- destImageName: "b/zot-4-test",
- testCaseName: "Request-4",
- },
- {
- srcImageName: "zot-cve-test",
- srcImageTag: "0.0.1",
- destImageName: "zot-5-test",
- testCaseName: "Request-5",
- },
- {
- srcImageName: "zot-cve-test",
- srcImageTag: "0.0.1",
- destImageName: "zot-1-test",
- testCaseName: "Request-6",
- },
- {
- srcImageName: "zot-cve-test",
- srcImageTag: "0.0.1",
- destImageName: "zot-2-test",
- testCaseName: "Request-7",
- },
- {
- srcImageName: "zot-cve-test",
- srcImageTag: "0.0.1",
- destImageName: "zot-3-test",
- testCaseName: "Request-8",
- },
- {
- srcImageName: "zot-cve-test",
- srcImageTag: "0.0.1",
- destImageName: "zot-4-test",
- testCaseName: "Request-9",
- },
- {
- srcImageName: "zot-cve-test",
- srcImageTag: "0.0.1",
- destImageName: "zot-5-test",
- testCaseName: "Request-10",
- },
- {
- srcImageName: "zot-test",
- srcImageTag: "0.0.1",
- destImageName: "zot-1-test",
- destImageTag: "0.0.1",
- testCaseName: "Request-11",
- },
- {
- srcImageName: "zot-test",
- srcImageTag: "0.0.1",
- destImageName: "zot-2-test",
- testCaseName: "Request-12",
- },
- {
- srcImageName: "zot-cve-test",
- srcImageTag: "0.0.1",
- destImageName: "a/zot-3-test",
- testCaseName: "Request-13",
- },
- {
- srcImageName: "zot-cve-test",
- srcImageTag: "0.0.1",
- destImageName: "b/zot-4-test",
- testCaseName: "Request-14",
- },
- }
-
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
-
- conf := config.New()
- conf.HTTP.Port = port
- htpasswdPath := test.MakeHtpasswdFileFromString(getCredString(username, passphrase))
-
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- }
-
- dir := t.TempDir()
- firstSubDir := t.TempDir()
- secondSubDir := t.TempDir()
-
- subPaths := make(map[string]config.StorageConfig)
-
- subPaths["/a"] = config.StorageConfig{RootDirectory: firstSubDir}
- subPaths["/b"] = config.StorageConfig{RootDirectory: secondSubDir}
-
- ctlr := makeController(conf, dir)
- ctlr.Config.Storage.SubPaths = subPaths
-
- testImagesDir := t.TempDir()
- testImagesController := ociutils.GetDefaultStoreController(testImagesDir, ctlr.Log)
-
- err := WriteImageToFileSystem(CreateRandomImage(), "zot-test", "0.0.1", testImagesController)
- assert.Equal(t, err, nil, "Error should be nil")
-
- err = WriteImageToFileSystem(CreateRandomImage(), "zot-cve-test", "0.0.1", testImagesController)
- assert.Equal(t, err, nil, "Error should be nil")
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
-
- // without creds, should get access error
- for i, testcase := range testCases {
- testcase := testcase
- run := i
-
- t.Run(testcase.testCaseName, func(t *testing.T) {
- t.Parallel()
- client := resty.New()
-
- tagResponse, err := client.R().SetBasicAuth(username, passphrase).
- Get(baseURL + "/v2/" + testcase.destImageName + "/tags/list")
- assert.Equal(t, err, nil, "Error should be nil")
- assert.NotEqual(t, tagResponse.StatusCode(), http.StatusBadRequest, "bad request")
-
- manifestList := getAllManifests(path.Join(testImagesDir, testcase.srcImageName))
-
- for _, manifest := range manifestList {
- headResponse, err := client.R().SetBasicAuth(username, passphrase).
- Head(baseURL + "/v2/" + testcase.destImageName + "/manifests/" + manifest)
- assert.Equal(t, err, nil, "Error should be nil")
- assert.Equal(t, headResponse.StatusCode(), http.StatusNotFound, "response status code should return 404")
-
- getResponse, err := client.R().SetBasicAuth(username, passphrase).
- Get(baseURL + "/v2/" + testcase.destImageName + "/manifests/" + manifest)
- assert.Equal(t, err, nil, "Error should be nil")
- assert.Equal(t, getResponse.StatusCode(), http.StatusNotFound, "response status code should return 404")
- }
-
- blobList := getAllBlobs(path.Join(testImagesDir, testcase.srcImageName))
-
- for _, blob := range blobList {
- // Get request of blob
- headResponse, err := client.R().
- SetBasicAuth(username, passphrase).
- Head(baseURL + "/v2/" + testcase.destImageName + "/blobs/sha256:" + blob)
-
- assert.Equal(t, err, nil, "Should not be nil")
- assert.NotEqual(t, headResponse.StatusCode(), http.StatusInternalServerError,
- "internal server error should not occurred")
-
- getResponse, err := client.R().
- SetBasicAuth(username, passphrase).
- Get(baseURL + "/v2/" + testcase.destImageName + "/blobs/sha256:" + blob)
-
- assert.Equal(t, err, nil, "Should not be nil")
- assert.NotEqual(t, getResponse.StatusCode(), http.StatusInternalServerError,
- "internal server error should not occurred")
-
- blobPath := path.Join(testImagesDir, testcase.srcImageName, "blobs/sha256", blob)
-
- buf, err := os.ReadFile(blobPath)
- if err != nil {
- panic(err)
- }
-
- // Post request of blob
- postResponse, err := client.R().
- SetHeader("Content-type", "application/octet-stream").
- SetBasicAuth(username, passphrase).
- SetBody(buf).Post(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/")
-
- assert.Equal(t, err, nil, "Error should be nil")
- assert.NotEqual(t, postResponse.StatusCode(), http.StatusInternalServerError,
- "response status code should not return 500")
-
- // Post request with query parameter
- if run%2 == 0 {
- postResponse, err = client.R().
- SetHeader("Content-type", "application/octet-stream").
- SetBasicAuth(username, passphrase).
- SetBody(buf).
- Post(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/")
-
- assert.Equal(t, err, nil, "Error should be nil")
- assert.NotEqual(t, postResponse.StatusCode(), http.StatusInternalServerError,
- "response status code should not return 500")
-
- var sessionID string
- sessionIDList := postResponse.Header().Values("Blob-Upload-UUID")
- if len(sessionIDList) == 0 {
- location := postResponse.Header().Values("Location")
- firstLocation := location[0]
- splitLocation := strings.Split(firstLocation, "/")
- sessionID = splitLocation[len(splitLocation)-1]
- } else {
- sessionID = sessionIDList[0]
- }
-
- file, err := os.Open(blobPath)
- if err != nil {
- panic(err)
- }
-
- defer file.Close()
-
- reader := bufio.NewReader(file)
-
- buf := make([]byte, 5*1024*1024)
-
- if run%4 == 0 {
- readContent := 0
- for {
- nbytes, err := reader.Read(buf)
- if err != nil {
- if goerrors.Is(err, io.EOF) {
- break
- }
- panic(err)
- }
- // Patch request of blob
-
- patchResponse, err := client.R().
- SetBody(buf[0:nbytes]).
- SetHeader("Content-Type", "application/octet-stream").
- SetHeader("Content-Length", fmt.Sprintf("%d", nbytes)).
- SetHeader("Content-Range", fmt.Sprintf("%d", readContent)+"-"+fmt.Sprintf("%d", readContent+nbytes-1)).
- SetBasicAuth(username, passphrase).
- Patch(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/" + sessionID)
-
- assert.Equal(t, err, nil, "Error should be nil")
- assert.NotEqual(t, patchResponse.StatusCode(), http.StatusInternalServerError,
- "response status code should not return 500")
-
- readContent += nbytes
- }
- } else {
- for {
- nbytes, err := reader.Read(buf)
- if err != nil {
- if goerrors.Is(err, io.EOF) {
- break
- }
- panic(err)
- }
- // Patch request of blob
-
- patchResponse, err := client.R().SetBody(buf[0:nbytes]).SetHeader("Content-type", "application/octet-stream").
- SetBasicAuth(username, passphrase).
- Patch(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/" + sessionID)
- if err != nil {
- panic(err)
- }
-
- assert.Equal(t, err, nil, "Error should be nil")
- assert.NotEqual(t, patchResponse.StatusCode(), http.StatusInternalServerError,
- "response status code should not return 500")
- }
- }
- } else {
- postResponse, err = client.R().
- SetHeader("Content-type", "application/octet-stream").
- SetBasicAuth(username, passphrase).
- SetBody(buf).SetQueryParam("digest", "sha256:"+blob).
- Post(baseURL + "/v2/" + testcase.destImageName + "/blobs/uploads/")
-
- assert.Equal(t, err, nil, "Error should be nil")
- assert.NotEqual(t, postResponse.StatusCode(), http.StatusInternalServerError,
- "response status code should not return 500")
- }
-
- headResponse, err = client.R().
- SetBasicAuth(username, passphrase).
- Head(baseURL + "/v2/" + testcase.destImageName + "/blobs/sha256:" + blob)
-
- assert.Equal(t, err, nil, "Should not be nil")
- assert.NotEqual(t, headResponse.StatusCode(), http.StatusInternalServerError, "response should return success code")
-
- getResponse, err = client.R().
- SetBasicAuth(username, passphrase).
- Get(baseURL + "/v2/" + testcase.destImageName + "/blobs/sha256:" + blob)
-
- assert.Equal(t, err, nil, "Should not be nil")
- assert.NotEqual(t, getResponse.StatusCode(), http.StatusInternalServerError, "response should return success code")
- }
-
- tagResponse, err = client.R().SetBasicAuth(username, passphrase).
- Get(baseURL + "/v2/" + testcase.destImageName + "/tags/list")
- assert.Equal(t, err, nil, "Error should be nil")
- assert.Equal(t, tagResponse.StatusCode(), http.StatusOK, "response status code should return success code")
-
- repoResponse, err := client.R().SetBasicAuth(username, passphrase).
- Get(baseURL + constants.RoutePrefix + constants.ExtCatalogPrefix)
- assert.Equal(t, err, nil, "Error should be nil")
- assert.Equal(t, repoResponse.StatusCode(), http.StatusOK, "response status code should return success code")
- })
- }
-}
-
-func TestHardLink(t *testing.T) {
- Convey("Validate hard link", t, func() {
- port := test.GetFreePort()
- conf := config.New()
- conf.HTTP.Port = port
-
- dir := t.TempDir()
-
- err := os.Chmod(dir, 0o400)
- if err != nil {
- panic(err)
- }
-
- subDir := t.TempDir()
-
- err = os.Chmod(subDir, 0o400)
- if err != nil {
- panic(err)
- }
-
- ctlr := makeController(conf, dir)
- subPaths := make(map[string]config.StorageConfig)
-
- subPaths["/a"] = config.StorageConfig{RootDirectory: subDir, Dedupe: true}
- ctlr.Config.Storage.SubPaths = subPaths
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
-
- err = os.Chmod(dir, 0o644)
- if err != nil {
- panic(err)
- }
-
- err = os.Chmod(subDir, 0o644)
- if err != nil {
- panic(err)
- }
-
- So(ctlr.Config.Storage.Dedupe, ShouldEqual, false)
- })
-}
-
-func TestImageSignatures(t *testing.T) {
- Convey("Validate signatures", t, func() {
- // start a new server
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
-
- conf := config.New()
- conf.HTTP.Port = port
-
- dir := t.TempDir()
- ctlr := makeController(conf, dir)
- cm := test.NewControllerManager(ctlr)
- // this blocks
- cm.StartAndWait(port)
- defer cm.StopServer()
-
- repoName := "signed-repo"
- img := CreateRandomImage()
- content := img.ManifestDescriptor.Data
- digest := img.ManifestDescriptor.Digest
-
- err := UploadImage(img, baseURL, repoName, "1.0")
- So(err, ShouldBeNil)
-
- Convey("Validate cosign signatures", func() {
- cwd, err := os.Getwd()
- So(err, ShouldBeNil)
- defer func() { _ = os.Chdir(cwd) }()
- tdir := t.TempDir()
- _ = os.Chdir(tdir)
-
- // generate a keypair
- os.Setenv("COSIGN_PASSWORD", "")
- err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil)
- So(err, ShouldBeNil)
-
- annotations := []string{"tag=1.0"}
-
- // sign the image
- err = sign.SignCmd(&options.RootOptions{Verbose: true, Timeout: 1 * time.Minute},
- options.KeyOpts{KeyRef: path.Join(tdir, "cosign.key"), PassFunc: generate.GetPass},
- options.SignOptions{
- Registry: options.RegistryOptions{AllowInsecure: true},
- AnnotationOptions: options.AnnotationOptions{Annotations: annotations},
- Upload: true,
- },
- []string{fmt.Sprintf("localhost:%s/%s@%s", port, repoName, digest.String())})
- So(err, ShouldBeNil)
-
- // verify the image
- aopts := &options.AnnotationOptions{Annotations: annotations}
- amap, err := aopts.AnnotationsMap()
- So(err, ShouldBeNil)
- vrfy := verify.VerifyCommand{
- RegistryOptions: options.RegistryOptions{AllowInsecure: true},
- CheckClaims: true,
- KeyRef: path.Join(tdir, "cosign.pub"),
- Annotations: amap,
- IgnoreTlog: true,
- }
- err = vrfy.Exec(context.TODO(), []string{fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")})
- So(err, ShouldBeNil)
-
- // verify the image with incorrect tag
- aopts = &options.AnnotationOptions{Annotations: []string{"tag=2.0"}}
- amap, err = aopts.AnnotationsMap()
- So(err, ShouldBeNil)
- vrfy = verify.VerifyCommand{
- RegistryOptions: options.RegistryOptions{AllowInsecure: true},
- CheckClaims: true,
- KeyRef: path.Join(tdir, "cosign.pub"),
- Annotations: amap,
- IgnoreTlog: true,
- }
- err = vrfy.Exec(context.TODO(), []string{fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")})
- So(err, ShouldNotBeNil)
-
- // verify the image with incorrect key
- aopts = &options.AnnotationOptions{Annotations: []string{"tag=1.0"}}
- amap, err = aopts.AnnotationsMap()
- So(err, ShouldBeNil)
- vrfy = verify.VerifyCommand{
- CheckClaims: true,
- RegistryOptions: options.RegistryOptions{AllowInsecure: true},
- KeyRef: path.Join(tdir, "cosign.key"),
- Annotations: amap,
- IgnoreTlog: true,
- }
- err = vrfy.Exec(context.TODO(), []string{fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")})
- So(err, ShouldNotBeNil)
-
- // generate another keypair
- err = os.Remove(path.Join(tdir, "cosign.pub"))
- So(err, ShouldBeNil)
- err = os.Remove(path.Join(tdir, "cosign.key"))
- So(err, ShouldBeNil)
-
- os.Setenv("COSIGN_PASSWORD", "")
- err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil)
- So(err, ShouldBeNil)
-
- // verify the image with incorrect key
- aopts = &options.AnnotationOptions{Annotations: []string{"tag=1.0"}}
- amap, err = aopts.AnnotationsMap()
- So(err, ShouldBeNil)
- vrfy = verify.VerifyCommand{
- CheckClaims: true,
- RegistryOptions: options.RegistryOptions{AllowInsecure: true},
- KeyRef: path.Join(tdir, "cosign.pub"),
- Annotations: amap,
- IgnoreTlog: true,
- }
- err = vrfy.Exec(context.TODO(), []string{fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")})
- So(err, ShouldNotBeNil)
- })
-
- Convey("Validate notation signatures", func() {
- cwd, err := os.Getwd()
- So(err, ShouldBeNil)
- defer func() { _ = os.Chdir(cwd) }()
- tdir := t.TempDir()
- _ = os.Chdir(tdir)
-
- signature.NotationPathLock.Lock()
- defer signature.NotationPathLock.Unlock()
-
- signature.LoadNotationPath(tdir)
-
- err = signature.GenerateNotationCerts(tdir, "good")
- So(err, ShouldBeNil)
-
- err = signature.GenerateNotationCerts(tdir, "bad")
- So(err, ShouldBeNil)
-
- image := fmt.Sprintf("localhost:%s/%s:%s", port, repoName, "1.0")
- err = signature.SignWithNotation("good", image, tdir)
- So(err, ShouldBeNil)
-
- err = signature.VerifyWithNotation(image, tdir)
- So(err, ShouldBeNil)
-
- // check list
- sigs, err := signature.ListNotarySignatures(image, tdir)
- So(len(sigs), ShouldEqual, 1)
- So(err, ShouldBeNil)
-
- // check unsupported manifest media type
- resp, err := resty.R().SetHeader("Content-Type", "application/vnd.unsupported.image.manifest.v1+json").
- SetBody(content).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusUnsupportedMediaType)
-
- // check invalid content with artifact media type
- resp, err = resty.R().SetHeader("Content-Type", artifactspec.MediaTypeArtifactManifest).
- SetBody([]byte("bogus")).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
-
- Convey("Validate corrupted signature", func() {
- // verify with corrupted signature
- resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
- fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String()))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- var refs ispec.Index
- err = json.Unmarshal(resp.Body(), &refs)
- So(err, ShouldBeNil)
- So(len(refs.Manifests), ShouldEqual, 1)
- err = os.WriteFile(path.Join(dir, repoName, "blobs",
- strings.ReplaceAll(refs.Manifests[0].Digest.String(), ":", "/")), []byte("corrupt"), 0o600)
- So(err, ShouldBeNil)
- resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
- fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String()))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
-
- err = signature.VerifyWithNotation(image, tdir)
- So(err, ShouldNotBeNil)
- })
-
- Convey("Validate deleted signature", func() {
- // verify with corrupted signature
- resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
- fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String()))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- var refs ispec.Index
- err = json.Unmarshal(resp.Body(), &refs)
- So(err, ShouldBeNil)
- So(len(refs.Manifests), ShouldEqual, 1)
- err = os.Remove(path.Join(dir, repoName, "blobs",
- strings.ReplaceAll(refs.Manifests[0].Digest.String(), ":", "/")))
- So(err, ShouldBeNil)
- resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
- fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String()))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
-
- err = signature.VerifyWithNotation(image, tdir)
- So(err, ShouldNotBeNil)
- })
- })
-
- Convey("GetOrasReferrers", func() {
- // cover error paths
- resp, err := resty.R().Get(
- fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, "badRepo", "badDigest"))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
-
- resp, err = resty.R().Get(
- fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, "badDigest"))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
-
- resp, err = resty.R().Get(
- fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String()))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
-
- resp, err = resty.R().SetQueryParam("artifactType", "badArtifact").Get(
- fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, repoName, digest.String()))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
-
- resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
- fmt.Sprintf("%s/oras/artifacts/v1/%s/manifests/%s/referrers", baseURL, "badRepo", digest.String()))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- })
- })
-}
-
-func TestManifestValidation(t *testing.T) {
- Convey("Validate manifest", t, func() {
- // start a new server
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
-
- conf := config.New()
- conf.HTTP.Port = port
-
- dir := t.TempDir()
- ctlr := makeController(conf, dir)
- cm := test.NewControllerManager(ctlr)
- // this blocks
- cm.StartServer()
- time.Sleep(1000 * time.Millisecond)
- defer cm.StopServer()
-
- repoName := "validation"
- blobContent := []byte("this is a blob")
- blobDigest := godigest.FromBytes(blobContent)
- So(blobDigest, ShouldNotBeNil)
-
- img := CreateRandomImage()
- content := img.ManifestDescriptor.Data
- digest := img.ManifestDescriptor.Digest
- configDigest := img.ConfigDescriptor.Digest
- configBlob := img.ConfigDescriptor.Data
-
- err := UploadImage(img, baseURL, repoName, "1.0")
- So(err, ShouldBeNil)
-
- Convey("empty layers should pass validation", func() {
- // create a manifest
- manifest := ispec.Manifest{
- Config: ispec.Descriptor{
- MediaType: ispec.MediaTypeImageConfig,
- Size: int64(len(configBlob)),
- Digest: configDigest,
- },
- Layers: []ispec.Descriptor{},
- Annotations: map[string]string{
- "key": "val",
- },
- }
- manifest.SchemaVersion = 2
-
- mcontent, err := json.Marshal(manifest)
- So(err, ShouldBeNil)
-
- resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- })
-
- Convey("empty layers and schemaVersion missing should fail validation", func() {
- // create a manifest
- manifest := ispec.Manifest{
- Config: ispec.Descriptor{
- MediaType: ispec.MediaTypeImageConfig,
- Size: int64(len(configBlob)),
- Digest: configDigest,
- },
- Layers: []ispec.Descriptor{},
- Annotations: map[string]string{
- "key": "val",
- },
- }
-
- mcontent, err := json.Marshal(manifest)
- So(err, ShouldBeNil)
-
- resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
- })
-
- Convey("missing layer should fail validation", func() {
- missingLayer := []byte("missing layer")
- missingLayerDigest := godigest.FromBytes(missingLayer)
- // create a manifest
- manifest := ispec.Manifest{
- Config: ispec.Descriptor{
- MediaType: ispec.MediaTypeImageConfig,
- Size: int64(len(configBlob)),
- Digest: configDigest,
- },
- Layers: []ispec.Descriptor{
- {
- MediaType: "application/vnd.oci.image.layer.v1.tar",
- Digest: digest,
- Size: int64(len(content)),
- },
- {
- MediaType: "application/vnd.oci.image.layer.v1.tar",
- Digest: missingLayerDigest,
- Size: int64(len(missingLayer)),
- },
- },
- Annotations: map[string]string{
- "key": "val",
- },
- }
- manifest.SchemaVersion = 2
-
- mcontent, err := json.Marshal(manifest)
- So(err, ShouldBeNil)
-
- resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
- })
-
- Convey("wrong mediatype should fail validation", func() {
- // create a manifest
- manifest := ispec.Manifest{
- MediaType: "bad.mediatype",
- Config: ispec.Descriptor{
- MediaType: ispec.MediaTypeImageConfig,
- Size: int64(len(configBlob)),
- Digest: configDigest,
- },
- Layers: []ispec.Descriptor{
- {
- MediaType: "application/vnd.oci.image.layer.v1.tar",
- Digest: digest,
- Size: int64(len(content)),
- },
- },
- Annotations: map[string]string{
- "key": "val",
- },
- }
- manifest.SchemaVersion = 2
-
- mcontent, err := json.Marshal(manifest)
- So(err, ShouldBeNil)
-
- resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
- })
-
- Convey("multiarch image should pass validation", func() {
- index := ispec.Index{
- MediaType: ispec.MediaTypeImageIndex,
- Manifests: []ispec.Descriptor{
- {
- MediaType: ispec.MediaTypeImageManifest,
- Digest: digest,
- Size: int64(len((content))),
- },
- },
- }
-
- index.SchemaVersion = 2
-
- indexContent, err := json.Marshal(index)
- So(err, ShouldBeNil)
-
- resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- SetBody(indexContent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/index", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- })
-
- Convey("multiarch image without schemaVersion should fail validation", func() {
- index := ispec.Index{
- MediaType: ispec.MediaTypeImageIndex,
- Manifests: []ispec.Descriptor{
- {
- MediaType: ispec.MediaTypeImageManifest,
- Digest: digest,
- Size: int64(len((content))),
- },
- },
- }
-
- indexContent, err := json.Marshal(index)
- So(err, ShouldBeNil)
-
- resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- SetBody(indexContent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/index", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
- })
-
- Convey("multiarch image with missing manifest should fail validation", func() {
- index := ispec.Index{
- MediaType: ispec.MediaTypeImageIndex,
- Manifests: []ispec.Descriptor{
- {
- MediaType: ispec.MediaTypeImageManifest,
- Digest: digest,
- Size: int64(len((content))),
- },
- {
- MediaType: ispec.MediaTypeImageManifest,
- Digest: godigest.FromString("missing layer"),
- Size: 10,
- },
- },
- }
-
- index.SchemaVersion = 2
-
- indexContent, err := json.Marshal(index)
- So(err, ShouldBeNil)
-
- resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- SetBody(indexContent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/index", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
- })
- })
-}
-
-func TestArtifactReferences(t *testing.T) {
- Convey("Validate Artifact References", t, func() {
- // start a new server
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
-
- conf := config.New()
- conf.HTTP.Port = port
-
- dir := t.TempDir()
- ctlr := makeController(conf, dir)
- cm := test.NewControllerManager(ctlr)
- // this blocks
- cm.StartServer()
- time.Sleep(1000 * time.Millisecond)
- defer cm.StopServer()
-
- repoName := "artifact-repo"
- content := []byte("this is a blob")
- digest := godigest.FromBytes(content)
- So(digest, ShouldNotBeNil)
-
- cfg, layers, manifest, err := deprecated.GetImageComponents(2) //nolint:staticcheck
- So(err, ShouldBeNil)
-
- err = UploadImage(
- Image{
- Config: cfg,
- Layers: layers,
- Manifest: manifest,
- }, baseURL, repoName, "1.0")
- So(err, ShouldBeNil)
-
- content, err = json.Marshal(manifest)
- So(err, ShouldBeNil)
- digest = godigest.FromBytes(content)
- So(digest, ShouldNotBeNil)
-
- artifactType := "application/vnd.example.icecream.v1"
-
- Convey("Validate Image Manifest Reference", func() {
- resp, err := resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- var referrers ispec.Index
- err = json.Unmarshal(resp.Body(), &referrers)
- So(err, ShouldBeNil)
- So(referrers.Manifests, ShouldBeEmpty)
-
- // now upload a reference
-
- // upload image config blob
- resp, err = resty.R().Post(baseURL + fmt.Sprintf("/v2/%s/blobs/uploads/", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
- loc := test.Location(baseURL, resp)
- cblob, cdigest := getEmptyImageConfig()
-
- resp, err = resty.R().
- SetContentLength(true).
- 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.StatusCode(), ShouldEqual, http.StatusCreated)
-
- // create a manifest
- manifest := ispec.Manifest{
- Config: ispec.Descriptor{
- MediaType: artifactType,
- Digest: cdigest,
- Size: int64(len(cblob)),
- },
- Layers: []ispec.Descriptor{
- {
- MediaType: "application/vnd.oci.image.layer.v1.tar",
- Digest: digest,
- Size: int64(len(content)),
- },
- },
- Subject: &ispec.Descriptor{
- MediaType: ispec.MediaTypeImageManifest,
- Digest: digest,
- Size: int64(len(content)),
- },
- Annotations: map[string]string{
- "key": "val",
- },
- }
- manifest.SchemaVersion = 2
-
- Convey("Using invalid content", func() {
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody([]byte("invalid data")).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
-
- // unknown repo will return status not found
- resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", "unknown", digest.String()))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
-
- resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- resp, err = resty.R().SetQueryParams(map[string]string{"artifactType": artifactType}).
- Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- // create a bad manifest (constructed manually)
- content := `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:71dbae9d7e6445fb5e0b11328e941b8e8937fdd52465079f536ce44bb78796ed","size":406}}` //nolint: lll
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody(content).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
-
- // missing layers
- mcontent := []byte("this is a missing blob")
- digest = godigest.FromBytes(mcontent)
- So(digest, ShouldNotBeNil)
-
- manifest.Layers = append(manifest.Layers, ispec.Descriptor{
- MediaType: "application/vnd.oci.image.layer.v1.tar",
- Digest: digest,
- Size: int64(len(mcontent)),
- })
-
- mcontent, err = json.Marshal(manifest)
- So(err, ShouldBeNil)
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
-
- // invalid schema version
- manifest.SchemaVersion = 1
-
- mcontent, err = json.Marshal(manifest)
- So(err, ShouldBeNil)
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
-
- // upload image config blob
- resp, err = resty.R().Post(baseURL + fmt.Sprintf("/v2/%s/blobs/uploads/", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
- loc := test.Location(baseURL, resp)
- cblob = []byte("{}")
- cdigest = godigest.FromBytes(cblob)
- So(cdigest, ShouldNotBeNil)
-
- resp, err = resty.R().
- SetContentLength(true).
- 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.StatusCode(), ShouldEqual, http.StatusCreated)
-
- manifest := ispec.Manifest{
- Config: ispec.Descriptor{
- MediaType: "application/vnd.oci.image.config.v1+json",
- Digest: cdigest,
- Size: int64(len(cblob)),
- },
- }
-
- manifest.SchemaVersion = 2
- mcontent, err = json.Marshal(manifest)
- So(err, ShouldBeNil)
- digest = godigest.FromBytes(mcontent)
- So(digest, ShouldNotBeNil)
-
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
-
- // missing layers
- mcontent = []byte("this is a missing blob")
- digest = godigest.FromBytes(mcontent)
- So(digest, ShouldNotBeNil)
-
- manifest.Layers = append(manifest.Layers, ispec.Descriptor{
- MediaType: "application/vnd.oci.image.layer.v1.tar",
- Digest: digest,
- Size: int64(len(mcontent)),
- })
-
- mcontent, err = json.Marshal(manifest)
- So(err, ShouldBeNil)
-
- // should fail because config is of type image and blob is not uploaded
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
-
- // no layers at all
- manifest.Layers = []ispec.Descriptor{}
-
- mcontent, err = json.Marshal(manifest)
- So(err, ShouldBeNil)
-
- // should not fail
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody(mcontent).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- })
-
- Convey("Using valid content", func() {
- content, err = json.Marshal(manifest)
- So(err, ShouldBeNil)
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody(content).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/1.0", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
-
- resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- resp, err = resty.R().SetQueryParams(map[string]string{"artifact": "invalid"}).
- Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- resp, err = resty.R().SetQueryParams(map[string]string{"artifactType": "invalid"}).
- Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- resp, err = resty.R().SetQueryParams(map[string]string{"artifactType": artifactType}).
- Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName, digest.String()))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- So(resp.Header().Get("Content-Type"), ShouldEqual, ispec.MediaTypeImageIndex)
- So(resp.Header().Get("OCI-Filters-Applied"), ShouldEqual, "artifactType")
-
- resp, err = resty.R().SetQueryParams(map[string]string{"artifactType": artifactType +
- ",otherArtType"}).Get(baseURL + fmt.Sprintf("/v2/%s/referrers/%s", repoName,
- digest.String()))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- So(resp.Header().Get("Content-Type"), ShouldEqual, ispec.MediaTypeImageIndex)
- So(resp.Header().Get("OCI-Filters-Applied"), ShouldEqual, "artifactType")
- })
- })
- })
-}
-
-//nolint:dupl // duplicated test code
-func TestRouteFailures(t *testing.T) {
- Convey("Make a new controller", t, func() {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- conf := config.New()
- conf.HTTP.Port = port
-
- ctlr := makeController(conf, t.TempDir())
- ctlr.Config.Storage.Commit = true
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
-
- rthdlr := api.NewRouteHandler(ctlr)
-
- // NOTE: the url or method itself doesn't matter below since we are calling the handlers directly,
- // so path routing is bypassed
-
- Convey("List tags", func() {
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- mux.SetURLVars(request, map[string]string{})
- response := httptest.NewRecorder()
-
- rthdlr.ListTags(response, request)
-
- resp := response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- response = httptest.NewRecorder()
-
- rthdlr.ListTags(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- qparm := request.URL.Query()
- qparm.Add("n", "a")
- request.URL.RawQuery = qparm.Encode()
- response = httptest.NewRecorder()
-
- rthdlr.ListTags(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- qparm = request.URL.Query()
- qparm.Add("n", "-1")
- request.URL.RawQuery = qparm.Encode()
- response = httptest.NewRecorder()
-
- rthdlr.ListTags(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- qparm = request.URL.Query()
- qparm.Add("n", "abc")
- request.URL.RawQuery = qparm.Encode()
- response = httptest.NewRecorder()
-
- rthdlr.ListTags(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- qparm = request.URL.Query()
- qparm.Add("n", "a")
- qparm.Add("n", "abc")
- request.URL.RawQuery = qparm.Encode()
- response = httptest.NewRecorder()
-
- rthdlr.ListTags(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- qparm = request.URL.Query()
- qparm.Add("n", "0")
- request.URL.RawQuery = qparm.Encode()
- response = httptest.NewRecorder()
-
- rthdlr.ListTags(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- qparm = request.URL.Query()
- qparm.Add("n", "1")
- qparm.Add("last", "")
- request.URL.RawQuery = qparm.Encode()
- response = httptest.NewRecorder()
-
- rthdlr.ListTags(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- qparm = request.URL.Query()
- qparm.Add("n", "1")
- qparm.Add("last", "a")
- request.URL.RawQuery = qparm.Encode()
- response = httptest.NewRecorder()
-
- rthdlr.ListTags(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- qparm = request.URL.Query()
- qparm.Add("last", "a")
- request.URL.RawQuery = qparm.Encode()
- response = httptest.NewRecorder()
-
- rthdlr.ListTags(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- qparm = request.URL.Query()
- qparm.Add("n", "1")
- qparm.Add("last", "a")
- qparm.Add("last", "abc")
- request.URL.RawQuery = qparm.Encode()
- response = httptest.NewRecorder()
-
- rthdlr.ListTags(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
- })
-
- Convey("Check manifest", func() {
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodHead, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{})
- response := httptest.NewRecorder()
-
- rthdlr.CheckManifest(response, request)
-
- resp := response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodHead, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- response = httptest.NewRecorder()
-
- rthdlr.CheckManifest(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodHead, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo", "reference": ""})
- response = httptest.NewRecorder()
-
- rthdlr.CheckManifest(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- })
-
- Convey("Get manifest", func() {
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{})
- response := httptest.NewRecorder()
-
- rthdlr.GetManifest(response, request)
-
- resp := response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- response = httptest.NewRecorder()
-
- rthdlr.GetManifest(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo", "reference": ""})
- response = httptest.NewRecorder()
-
- rthdlr.GetManifest(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- })
-
- Convey("Update manifest", func() {
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{})
- response := httptest.NewRecorder()
-
- rthdlr.UpdateManifest(response, request)
-
- resp := response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- response = httptest.NewRecorder()
-
- rthdlr.UpdateManifest(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo", "reference": ""})
- response = httptest.NewRecorder()
-
- rthdlr.UpdateManifest(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- })
-
- Convey("Delete manifest", func() {
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{})
- response := httptest.NewRecorder()
-
- rthdlr.DeleteManifest(response, request)
-
- resp := response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- response = httptest.NewRecorder()
-
- rthdlr.DeleteManifest(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo", "reference": ""})
- response = httptest.NewRecorder()
-
- rthdlr.DeleteManifest(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- })
-
- Convey("Check blob", func() {
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodHead, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{})
- response := httptest.NewRecorder()
-
- rthdlr.CheckBlob(response, request)
-
- resp := response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodHead, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- response = httptest.NewRecorder()
-
- rthdlr.CheckBlob(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodHead, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo", "digest": ""})
- response = httptest.NewRecorder()
-
- rthdlr.CheckBlob(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- })
-
- Convey("Get blob", func() {
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{})
- response := httptest.NewRecorder()
-
- rthdlr.GetBlob(response, request)
-
- resp := response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- response = httptest.NewRecorder()
-
- rthdlr.GetBlob(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo", "digest": ""})
- response = httptest.NewRecorder()
-
- rthdlr.GetBlob(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- })
-
- Convey("Delete blob", func() {
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{})
- response := httptest.NewRecorder()
-
- rthdlr.DeleteBlob(response, request)
-
- resp := response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- response = httptest.NewRecorder()
-
- rthdlr.DeleteBlob(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo", "digest": ""})
- response = httptest.NewRecorder()
-
- rthdlr.DeleteBlob(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- })
-
- Convey("Create blob upload", func() {
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPost, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{})
- response := httptest.NewRecorder()
-
- rthdlr.CreateBlobUpload(response, request)
-
- resp := response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPost, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- qparm := request.URL.Query()
- qparm.Add("mount", "a")
- qparm.Add("mount", "abc")
- request.URL.RawQuery = qparm.Encode()
- response = httptest.NewRecorder()
-
- rthdlr.CreateBlobUpload(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPost, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- qparm = request.URL.Query()
- qparm.Add("mount", "a")
- request.URL.RawQuery = qparm.Encode()
- response = httptest.NewRecorder()
-
- rthdlr.CreateBlobUpload(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusAccepted)
- })
-
- Convey("Get blob upload", func() {
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{})
- response := httptest.NewRecorder()
-
- rthdlr.GetBlobUpload(response, request)
-
- resp := response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- response = httptest.NewRecorder()
-
- rthdlr.GetBlobUpload(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- })
-
- Convey("Patch blob upload", func() {
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPatch, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{})
- response := httptest.NewRecorder()
-
- rthdlr.PatchBlobUpload(response, request)
-
- resp := response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- response = httptest.NewRecorder()
-
- rthdlr.PatchBlobUpload(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- })
-
- Convey("Update blob upload", func() {
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{})
- response := httptest.NewRecorder()
-
- rthdlr.UpdateBlobUpload(response, request)
-
- resp := response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- response = httptest.NewRecorder()
-
- rthdlr.UpdateBlobUpload(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo", "session_id": "bar"})
- response = httptest.NewRecorder()
-
- rthdlr.UpdateBlobUpload(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo", "session_id": "bar"})
- qparm := request.URL.Query()
- qparm.Add("digest", "a")
- qparm.Add("digest", "abc")
- request.URL.RawQuery = qparm.Encode()
- response = httptest.NewRecorder()
-
- rthdlr.UpdateBlobUpload(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
- })
-
- Convey("Delete blob upload", func() {
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{})
- response := httptest.NewRecorder()
-
- rthdlr.DeleteBlobUpload(response, request)
-
- resp := response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- response = httptest.NewRecorder()
-
- rthdlr.DeleteBlobUpload(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- })
-
- Convey("Get referrers", func() {
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{})
- response := httptest.NewRecorder()
-
- rthdlr.GetOrasReferrers(response, request)
-
- resp := response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
-
- request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "foo"})
- response = httptest.NewRecorder()
-
- rthdlr.GetOrasReferrers(response, request)
-
- resp = response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
- })
- })
-}
-
-func TestListingTags(t *testing.T) {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- conf := config.New()
- conf.HTTP.Port = port
-
- ctlr := makeController(conf, t.TempDir())
- ctlr.Config.Storage.Commit = true
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
-
- defer cm.StopServer()
-
- rthdlr := api.NewRouteHandler(ctlr)
-
- img := CreateRandomImage()
- sigTag := fmt.Sprintf("sha256-%s.sig", img.Digest().Encoded())
-
- repoName := "test-tags"
- tagsList := []string{
- "1", "2", "1.0.0", "new", "2.0.0", sigTag,
- "2-test", "New", "2.0.0-test", "latest",
- }
-
- for _, tag := range tagsList {
- err := UploadImage(img, baseURL, repoName, tag)
- if err != nil {
- panic(err)
- }
- }
-
- // Note empty strings signify the query parameter is not set
- // There are separate tests for passing the empty string as query parameter
- testCases := []struct {
- testCaseName string
- pageSize string
- last string
- expectedTags []string
- }{
- {
- testCaseName: "Test tag soting order with no parameters",
- pageSize: "",
- last: "",
- expectedTags: []string{
- "1", "1.0.0", "2", "2-test", "2.0.0", "2.0.0-test",
- "New", "latest", "new", sigTag,
- },
- },
- {
- testCaseName: "Test with the parameter 'n' lower than total number of results",
- pageSize: "5",
- last: "",
- expectedTags: []string{
- "1", "1.0.0", "2", "2-test", "2.0.0",
- },
- },
- {
- testCaseName: "Test with the parameter 'n' larger than total number of results",
- pageSize: "50",
- last: "",
- expectedTags: []string{
- "1", "1.0.0", "2", "2-test", "2.0.0", "2.0.0-test",
- "New", "latest", "new", sigTag,
- },
- },
- {
- testCaseName: "Test the parameter 'n' is 0",
- pageSize: "0",
- last: "",
- expectedTags: []string{},
- },
- {
- testCaseName: "Test the parameters 'n' and 'last'",
- pageSize: "5",
- last: "2-test",
- expectedTags: []string{"2.0.0", "2.0.0-test", "New", "latest", "new"},
- },
- {
- testCaseName: "Test the parameters 'n' and 'last' with `n` exceeding total number of results",
- pageSize: "5",
- last: "latest",
- expectedTags: []string{"new", sigTag},
- },
- {
- testCaseName: "Test the parameter 'n' and 'last' being second to last tag as value",
- pageSize: "2",
- last: "new",
- expectedTags: []string{sigTag},
- },
- {
- testCaseName: "Test the parameter 'last' without parameter 'n'",
- pageSize: "",
- last: "2",
- expectedTags: []string{
- "2-test", "2.0.0", "2.0.0-test",
- "New", "latest", "new", sigTag,
- },
- },
- {
- testCaseName: "Test the parameter 'last' with the final tag as value",
- pageSize: "",
- last: sigTag,
- expectedTags: []string{},
- },
- {
- testCaseName: "Test the parameter 'last' with the second to last tag as value",
- pageSize: "",
- last: "new",
- expectedTags: []string{sigTag},
- },
- }
-
- for _, testCase := range testCases {
- Convey(testCase.testCaseName, t, func() {
- t.Log("Running " + testCase.testCaseName)
-
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": repoName})
-
- if testCase.pageSize != "" || testCase.last != "" {
- qparm := request.URL.Query()
-
- if testCase.pageSize != "" {
- qparm.Add("n", testCase.pageSize)
- }
-
- if testCase.last != "" {
- qparm.Add("last", testCase.last)
- }
-
- request.URL.RawQuery = qparm.Encode()
- }
-
- response := httptest.NewRecorder()
-
- rthdlr.ListTags(response, request)
-
- resp := response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusOK)
-
- var tags common.ImageTags
- err := json.NewDecoder(resp.Body).Decode(&tags)
- So(err, ShouldBeNil)
- So(tags.Tags, ShouldEqual, testCase.expectedTags)
-
- alltags := tagsList
- sort.Strings(alltags)
-
- actualLinkValue := resp.Header.Get("Link")
- if testCase.pageSize == "0" || testCase.pageSize == "" { //nolint:gocritic
- So(actualLinkValue, ShouldEqual, "")
- } else if testCase.expectedTags[len(testCase.expectedTags)-1] == alltags[len(alltags)-1] {
- So(actualLinkValue, ShouldEqual, "")
- } else {
- expectedLinkValue := fmt.Sprintf("/v2/%s/tags/list?n=%s&last=%s; rel=\"next\"",
- repoName, testCase.pageSize, tags.Tags[len(tags.Tags)-1],
- )
- So(actualLinkValue, ShouldEqual, expectedLinkValue)
- }
-
- t.Log("Finished " + testCase.testCaseName)
- })
- }
-}
-
-func TestStorageCommit(t *testing.T) {
- Convey("Make a new controller", t, func() {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- conf := config.New()
- conf.HTTP.Port = port
-
- dir := t.TempDir()
- ctlr := makeController(conf, dir)
- ctlr.Config.Storage.Commit = true
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
-
- Convey("Manifests", func() {
- _, _ = Print("\nManifests")
-
- cfg, layers, manifest, err := deprecated.GetImageComponents(2) //nolint:staticcheck
- So(err, ShouldBeNil)
-
- content := []byte("this is a blob5")
- digest := godigest.FromBytes(content)
- So(digest, ShouldNotBeNil)
- // check a non-existent manifest
- resp, err := resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
- SetBody(content).Head(baseURL + "/v2/unknown/manifests/test:1.0")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
-
- repoName := "repo7"
- err = UploadImage(
- Image{
- Config: cfg,
- Layers: layers,
- Manifest: manifest,
- }, baseURL, repoName, "test:1.0")
- So(err, ShouldBeNil)
-
- _, err = os.Stat(path.Join(dir, "repo7"))
- So(err, ShouldBeNil)
-
- content, err = json.Marshal(manifest)
- So(err, ShouldBeNil)
- digest = godigest.FromBytes(content)
- So(digest, ShouldNotBeNil)
- resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
- SetBody(content).Put(baseURL + "/v2/repo7/manifests/test:1.0")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- digestHdr := resp.Header().Get(constants.DistContentDigestKey)
- So(digestHdr, ShouldNotBeEmpty)
- So(digestHdr, ShouldEqual, digest.String())
-
- resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
- SetBody(content).Put(baseURL + "/v2/repo7/manifests/test:1.0.1")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- digestHdr = resp.Header().Get(constants.DistContentDigestKey)
- So(digestHdr, ShouldNotBeEmpty)
- So(digestHdr, ShouldEqual, digest.String())
-
- content, err = json.Marshal(manifest)
- So(err, ShouldBeNil)
-
- err = UploadImage(
- Image{
- Config: cfg,
- Layers: layers,
- Manifest: manifest,
- }, baseURL, repoName, "test:1.0.1")
- So(err, ShouldBeNil)
-
- cfg, layers, manifest, err = deprecated.GetImageComponents(1) //nolint:staticcheck
- So(err, ShouldBeNil)
-
- err = UploadImage(
- Image{
- Config: cfg,
- Layers: layers,
- Manifest: manifest,
- }, baseURL, repoName, "test:2.0")
- So(err, ShouldBeNil)
-
- resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
- SetBody(content).Put(baseURL + "/v2/repo7/manifests/test:2.0")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- digestHdr = resp.Header().Get(constants.DistContentDigestKey)
- So(digestHdr, ShouldNotBeEmpty)
- So(digestHdr, ShouldEqual, digest.String())
-
- // check/get by tag
- resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/test:1.0")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
- resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/test:1.0")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- So(resp.Body(), ShouldNotBeEmpty)
- // check/get by reference
- resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/" + digest.String())
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
- resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/" + digest.String())
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- So(resp.Body(), ShouldNotBeEmpty)
-
- // delete manifest by tag should pass
- resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/test:1.0")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
- // delete manifest by digest (1.0 deleted but 1.0.1 has same reference)
- resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String())
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
- // delete manifest by digest
- resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String())
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- // delete again should fail
- resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String())
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
-
- // check/get by tag
- resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/test:1.0")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/test:1.0")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- So(resp.Body(), ShouldNotBeEmpty)
- resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/test:2.0")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/test:2.0")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- So(resp.Body(), ShouldNotBeEmpty)
- // check/get by reference
- resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/" + digest.String())
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/" + digest.String())
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- So(resp.Body(), ShouldNotBeEmpty)
- })
- })
-}
-
-func TestManifestImageIndex(t *testing.T) {
- Convey("Make a new controller", t, func() {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- conf := config.New()
- conf.HTTP.Port = port
-
- dir := t.TempDir()
- ctlr := makeController(conf, dir)
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
-
- rthdlr := api.NewRouteHandler(ctlr)
-
- cfg, layers, manifest, err := deprecated.GetImageComponents(2) //nolint:staticcheck
- So(err, ShouldBeNil)
-
- content := []byte("this is a blob1")
- digest := godigest.FromBytes(content)
- So(digest, ShouldNotBeNil)
-
- // check a non-existent manifest
- resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody(content).Head(baseURL + "/v2/unknown/manifests/test:1.0")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
-
- repoName := "index"
- err = UploadImage(
- Image{
- Config: cfg,
- Layers: layers,
- Manifest: manifest,
- }, baseURL, repoName, "test:1.0")
- So(err, ShouldBeNil)
-
- _, err = os.Stat(path.Join(dir, "index"))
- So(err, ShouldBeNil)
-
- content, err = json.Marshal(manifest)
- So(err, ShouldBeNil)
- digest = godigest.FromBytes(content)
- So(digest, ShouldNotBeNil)
- m1content := content
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody(content).Put(baseURL + "/v2/index/manifests/test:1.0")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- digestHdr := resp.Header().Get(constants.DistContentDigestKey)
- So(digestHdr, ShouldNotBeEmpty)
- So(digestHdr, ShouldEqual, digest.String())
-
- // create another manifest but upload using its sha256 reference
-
- // upload image config blob
- resp, err = resty.R().Post(baseURL + "/v2/index/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
-
- img := CreateRandomImage()
-
- err = UploadImage(img, baseURL, repoName, img.DigestStr())
- So(err, ShouldBeNil)
-
- content = img.ManifestDescriptor.Data
- digest = img.ManifestDescriptor.Digest
-
- m2dgst := digest
- m2size := len(content)
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody(content).Put(baseURL + fmt.Sprintf("/v2/index/manifests/%s", digest))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- digestHdr = resp.Header().Get(constants.DistContentDigestKey)
- So(digestHdr, ShouldNotBeEmpty)
- So(digestHdr, ShouldEqual, digest.String())
-
- Convey("Image index", func() {
- img := CreateRandomImage()
-
- err = UploadImage(img, baseURL, repoName, img.DigestStr())
- So(err, ShouldBeNil)
-
- content := img.ManifestDescriptor.Data
- digest = img.ManifestDescriptor.Digest
-
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody(content).Put(baseURL + fmt.Sprintf("/v2/index/manifests/%s", digest.String()))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- digestHdr := resp.Header().Get(constants.DistContentDigestKey)
- So(digestHdr, ShouldNotBeEmpty)
- So(digestHdr, ShouldEqual, digest.String())
-
- var index ispec.Index
- index.SchemaVersion = 2
- index.Manifests = []ispec.Descriptor{
- {
- MediaType: ispec.MediaTypeImageManifest,
- Digest: digest,
- Size: int64(len(content)),
- },
- {
- MediaType: ispec.MediaTypeImageManifest,
- Digest: m2dgst,
- Size: int64(m2size),
- },
- }
-
- content, err = json.Marshal(index)
- So(err, ShouldBeNil)
- digest = godigest.FromBytes(content)
- So(digest, ShouldNotBeNil)
- index1dgst := digest
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- SetBody(content).Put(baseURL + "/v2/index/manifests/test:index1")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- digestHdr = resp.Header().Get(constants.DistContentDigestKey)
- So(digestHdr, ShouldNotBeEmpty)
- So(digestHdr, ShouldEqual, digest.String())
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- Get(baseURL + "/v2/index/manifests/test:index1")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- So(resp.Body(), ShouldNotBeEmpty)
- So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
-
- img = CreateRandomImage()
-
- err = UploadImage(img, baseURL, repoName, img.DigestStr())
- So(err, ShouldBeNil)
-
- content = img.ManifestDescriptor.Data
- digest = img.ManifestDescriptor.Digest
-
- m4dgst := digest
- m4size := len(content)
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody(content).Put(baseURL + fmt.Sprintf("/v2/index/manifests/%s", digest.String()))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- digestHdr = resp.Header().Get(constants.DistContentDigestKey)
- So(digestHdr, ShouldNotBeEmpty)
- So(digestHdr, ShouldEqual, digest.String())
-
- index.SchemaVersion = 2
- index.Manifests = []ispec.Descriptor{
- {
- MediaType: ispec.MediaTypeImageManifest,
- Digest: digest,
- Size: int64(len(content)),
- },
- {
- MediaType: ispec.MediaTypeImageManifest,
- Digest: m2dgst,
- Size: int64(m2size),
- },
- }
-
- content, err = json.Marshal(index)
- So(err, ShouldBeNil)
- digest = godigest.FromBytes(content)
- So(digest, ShouldNotBeNil)
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- SetBody(content).Put(baseURL + "/v2/index/manifests/test:index2")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- digestHdr = resp.Header().Get(constants.DistContentDigestKey)
- So(digestHdr, ShouldNotBeEmpty)
- So(digestHdr, ShouldEqual, digest.String())
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- Get(baseURL + "/v2/index/manifests/test:index2")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- So(resp.Body(), ShouldNotBeEmpty)
- So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
-
- Convey("List tags", func() {
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
- request = mux.SetURLVars(request, map[string]string{"name": "index"})
- response := httptest.NewRecorder()
-
- rthdlr.ListTags(response, request)
-
- resp := response.Result()
- defer resp.Body.Close()
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusOK)
-
- var tags common.ImageTags
- err = json.NewDecoder(resp.Body).Decode(&tags)
- So(err, ShouldBeNil)
- So(len(tags.Tags), ShouldEqual, 3)
- So(tags.Tags, ShouldContain, "test:1.0")
- So(tags.Tags, ShouldContain, "test:index1")
- So(tags.Tags, ShouldContain, "test:index2")
- })
-
- Convey("Another index with same manifest", func() {
- var index ispec.Index
- index.SchemaVersion = 2
- index.Manifests = []ispec.Descriptor{
- {
- MediaType: ispec.MediaTypeImageManifest,
- Digest: m4dgst,
- Size: int64(m4size),
- },
- }
-
- content, err = json.Marshal(index)
- So(err, ShouldBeNil)
- digest = godigest.FromBytes(content)
- So(digest, ShouldNotBeNil)
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- SetBody(content).Put(baseURL + "/v2/index/manifests/test:index3")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- digestHdr = resp.Header().Get(constants.DistContentDigestKey)
- So(digestHdr, ShouldNotBeEmpty)
- So(digestHdr, ShouldEqual, digest.String())
- })
-
- Convey("Another index using digest with same manifest", func() {
- var index ispec.Index
- index.SchemaVersion = 2
- index.Manifests = []ispec.Descriptor{
- {
- MediaType: ispec.MediaTypeImageManifest,
- Digest: m4dgst,
- Size: int64(m4size),
- },
- }
-
- content, err = json.Marshal(index)
- So(err, ShouldBeNil)
- digest = godigest.FromBytes(content)
- So(digest, ShouldNotBeNil)
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- SetBody(content).Put(baseURL + fmt.Sprintf("/v2/index/manifests/%s", digest))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- digestHdr = resp.Header().Get(constants.DistContentDigestKey)
- So(digestHdr, ShouldNotBeEmpty)
- So(digestHdr, ShouldEqual, digest.String())
- })
-
- Convey("Deleting manifest contained by a multiarch image should not be allowed", func() {
- resp, err = resty.R().Delete(baseURL + fmt.Sprintf("/v2/index/manifests/%s", m2dgst.String()))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
- })
-
- Convey("Deleting an image index", func() {
- // delete manifest by tag should pass
- resp, err = resty.R().Delete(baseURL + "/v2/index/manifests/test:index3")
- So(err, ShouldBeNil)
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- Get(baseURL + "/v2/index/manifests/test:index3")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- So(resp.Body(), ShouldNotBeEmpty)
- resp, err = resty.R().Delete(baseURL + "/v2/index/manifests/test:index1")
- So(err, ShouldBeNil)
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- Get(baseURL + "/v2/index/manifests/test:index1")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- So(resp.Body(), ShouldNotBeEmpty)
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- Get(baseURL + "/v2/index/manifests/test:index2")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- So(resp.Body(), ShouldNotBeEmpty)
- So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
- })
-
- Convey("Deleting an image index by digest", func() {
- // delete manifest by tag should pass
- resp, err = resty.R().Delete(baseURL + "/v2/index/manifests/test:index3")
- So(err, ShouldBeNil)
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- Get(baseURL + "/v2/index/manifests/test:index3")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- So(resp.Body(), ShouldNotBeEmpty)
- resp, err = resty.R().Delete(baseURL + fmt.Sprintf("/v2/index/manifests/%s", index1dgst))
- So(err, ShouldBeNil)
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- Get(baseURL + "/v2/index/manifests/test:index1")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- So(resp.Body(), ShouldNotBeEmpty)
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- Get(baseURL + "/v2/index/manifests/test:index2")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- So(resp.Body(), ShouldNotBeEmpty)
- So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
- })
-
- Convey("Update an index tag with different manifest", func() {
- img := CreateRandomImage()
-
- err = UploadImage(img, baseURL, repoName, img.DigestStr())
- So(err, ShouldBeNil)
-
- content = img.ManifestDescriptor.Data
- digest = img.ManifestDescriptor.Digest
-
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody(content).Put(baseURL + fmt.Sprintf("/v2/index/manifests/%s", digest))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- digestHdr = resp.Header().Get(constants.DistContentDigestKey)
- So(digestHdr, ShouldNotBeEmpty)
- So(digestHdr, ShouldEqual, digest.String())
-
- index.SchemaVersion = 2
- index.Manifests = []ispec.Descriptor{
- {
- MediaType: ispec.MediaTypeImageManifest,
- Digest: digest,
- Size: int64(len(content)),
- },
- }
-
- content, err = json.Marshal(index)
- So(err, ShouldBeNil)
- digest = godigest.FromBytes(content)
- So(digest, ShouldNotBeNil)
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- SetBody(content).Put(baseURL + "/v2/index/manifests/test:index1")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- digestHdr = resp.Header().Get(constants.DistContentDigestKey)
- So(digestHdr, ShouldNotBeEmpty)
- So(digestHdr, ShouldEqual, digest.String())
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- Get(baseURL + "/v2/index/manifests/test:index1")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- So(resp.Body(), ShouldNotBeEmpty)
- So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
-
- // delete manifest by tag should pass
- resp, err = resty.R().Delete(baseURL + "/v2/index/manifests/test:index1")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- Get(baseURL + "/v2/index/manifests/test:index1")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- So(resp.Body(), ShouldNotBeEmpty)
- })
-
- Convey("Negative test cases", func() {
- Convey("Delete index", func() {
- err = os.Remove(path.Join(dir, "index", "blobs", index1dgst.Algorithm().String(), index1dgst.Encoded()))
- So(err, ShouldBeNil)
- resp, err = resty.R().Delete(baseURL + fmt.Sprintf("/v2/index/manifests/%s", index1dgst))
- So(err, ShouldBeNil)
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- Get(baseURL + "/v2/index/manifests/test:index1")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- So(resp.Body(), ShouldNotBeEmpty)
- })
-
- Convey("Corrupt index", func() {
- err = os.WriteFile(path.Join(dir, "index", "blobs", index1dgst.Algorithm().String(), index1dgst.Encoded()),
- []byte("deadbeef"), storageConstants.DefaultFilePerms)
- So(err, ShouldBeNil)
- resp, err = resty.R().Delete(baseURL + fmt.Sprintf("/v2/index/manifests/%s", index1dgst))
- So(err, ShouldBeNil)
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- Get(baseURL + "/v2/index/manifests/test:index1")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
- So(resp.Body(), ShouldBeEmpty)
- })
-
- Convey("Change media-type", func() {
- // previously a manifest, try writing an image index
- var index ispec.Index
- index.SchemaVersion = 2
- index.Manifests = []ispec.Descriptor{
- {
- MediaType: ispec.MediaTypeImageIndex,
- Digest: m4dgst,
- Size: int64(m4size),
- },
- }
-
- content, err = json.Marshal(index)
- So(err, ShouldBeNil)
- digest = godigest.FromBytes(content)
- So(digest, ShouldNotBeNil)
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- SetBody(content).Put(baseURL + "/v2/index/manifests/test:1.0")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
-
- // previously an image index, try writing a manifest
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody(m1content).Put(baseURL + "/v2/index/manifests/test:index1")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
- })
- })
- })
- })
-}
-
-func TestManifestCollision(t *testing.T) {
- Convey("Make a new controller", t, func() {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- conf := config.New()
- conf.HTTP.Port = port
-
- dir := t.TempDir()
- ctlr := makeController(conf, dir)
-
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Repositories: config.Repositories{
- AuthorizationAllRepos: config.PolicyGroup{
- AnonymousPolicy: []string{
- constants.ReadPermission,
- constants.CreatePermission,
- constants.DeletePermission,
- constants.DetectManifestCollisionPermission,
- },
- },
- },
- }
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
-
- cfg, layers, manifest, err := deprecated.GetImageComponents(2) //nolint:staticcheck
- So(err, ShouldBeNil)
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodHead, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo", "digest": ""})
+ response = httptest.NewRecorder()
- err = UploadImage(
- Image{
- Config: cfg,
- Layers: layers,
- Manifest: manifest,
- }, baseURL, "index", "test:1.0")
- So(err, ShouldBeNil)
+ rthdlr.CheckBlob(response, request)
- _, err = os.Stat(path.Join(dir, "index"))
- So(err, ShouldBeNil)
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
+ })
- content := []byte("this is a blob1")
- digest := godigest.FromBytes(content)
- So(digest, ShouldNotBeNil)
+ Convey("Get blob", func() {
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{})
+ response := httptest.NewRecorder()
- // check a non-existent manifest
- resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
- SetBody(content).Head(baseURL + "/v2/unknown/manifests/test:1.0")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ rthdlr.GetBlob(response, request)
- content, err = json.Marshal(manifest)
- So(err, ShouldBeNil)
- digest = godigest.FromBytes(content)
- So(digest, ShouldNotBeNil)
+ resp := response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- err = UploadImage(
- Image{
- Config: cfg,
- Layers: layers,
- Manifest: manifest,
- }, baseURL, "index", "test:2.0")
- So(err, ShouldBeNil)
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ response = httptest.NewRecorder()
- // Deletion should fail if using digest
- resp, err = resty.R().Delete(baseURL + "/v2/index/manifests/" + digest.String())
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusConflict)
+ rthdlr.GetBlob(response, request)
- // remove detectManifestCollision action from ** (all repos)
- repoPolicy := conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos]
- repoPolicy.AnonymousPolicy = []string{"read", "delete"}
- conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- resp, err = resty.R().Delete(baseURL + "/v2/index/manifests/" + digest.String())
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo", "digest": ""})
+ response = httptest.NewRecorder()
- resp, err = resty.R().Get(baseURL + "/v2/index/manifests/test:1.0")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ rthdlr.GetBlob(response, request)
- resp, err = resty.R().Get(baseURL + "/v2/index/manifests/test:2.0")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- })
-}
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
+ })
-func TestPullRange(t *testing.T) {
- Convey("Make a new controller", t, func() {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- conf := config.New()
- conf.HTTP.Port = port
+ Convey("Delete blob", func() {
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{})
+ response := httptest.NewRecorder()
- dir := t.TempDir()
- ctlr := makeController(conf, dir)
+ rthdlr.DeleteBlob(response, request)
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ resp := response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- // create a blob/layer
- resp, err := resty.R().Post(baseURL + "/v2/index/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
- loc := test.Location(baseURL, resp)
- So(loc, ShouldNotBeEmpty)
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ response = httptest.NewRecorder()
- // since we are not specifying any prefix i.e provided in config while starting server,
- // so it should store index1 to global root dir
- _, err = os.Stat(path.Join(dir, "index"))
- So(err, ShouldBeNil)
+ rthdlr.DeleteBlob(response, request)
- resp, err = resty.R().Get(loc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
- content := []byte("0123456789")
- digest := godigest.FromBytes(content)
- So(digest, ShouldNotBeNil)
- // monolithic blob upload: success
- resp, err = resty.R().SetQueryParam("digest", digest.String()).
- SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- blobLoc := resp.Header().Get("Location")
- So(blobLoc, ShouldNotBeEmpty)
- So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
- So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty)
- blobLoc = baseURL + blobLoc
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- Convey("Range is supported using 'bytes'", func() {
- resp, err = resty.R().Head(blobLoc)
- So(err, ShouldBeNil)
- So(resp.Header().Get("Accept-Ranges"), ShouldEqual, "bytes")
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- })
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo", "digest": ""})
+ response = httptest.NewRecorder()
- Convey("Get a range of bytes", func() {
- resp, err = resty.R().SetHeader("Range", "bytes=0-").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusPartialContent)
- So(resp.Header().Get("Content-Length"), ShouldEqual, fmt.Sprintf("%d", len(content)))
- So(resp.Body(), ShouldResemble, content)
+ rthdlr.DeleteBlob(response, request)
- resp, err = resty.R().SetHeader("Range", "bytes=0-100").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusPartialContent)
- So(resp.Header().Get("Content-Length"), ShouldEqual, fmt.Sprintf("%d", len(content)))
- So(resp.Body(), ShouldResemble, content)
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
+ })
- resp, err = resty.R().SetHeader("Range", "bytes=0-10").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusPartialContent)
- So(resp.Header().Get("Content-Length"), ShouldEqual, fmt.Sprintf("%d", len(content)))
- So(resp.Body(), ShouldResemble, content)
+ Convey("Create blob upload", func() {
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPost, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{})
+ response := httptest.NewRecorder()
- resp, err = resty.R().SetHeader("Range", "bytes=0-0").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusPartialContent)
- So(resp.Header().Get("Content-Length"), ShouldEqual, "1")
- So(resp.Body(), ShouldResemble, content[0:1])
+ rthdlr.CreateBlobUpload(response, request)
- resp, err = resty.R().SetHeader("Range", "bytes=0-1").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusPartialContent)
- So(resp.Header().Get("Content-Length"), ShouldEqual, "2")
- So(resp.Body(), ShouldResemble, content[0:2])
+ resp := response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- resp, err = resty.R().SetHeader("Range", "bytes=2-3").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusPartialContent)
- So(resp.Header().Get("Content-Length"), ShouldEqual, "2")
- So(resp.Body(), ShouldResemble, content[2:4])
- })
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPost, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ qparm := request.URL.Query()
+ qparm.Add("mount", "a")
+ qparm.Add("mount", "abc")
+ request.URL.RawQuery = qparm.Encode()
+ response = httptest.NewRecorder()
- Convey("Negative cases", func() {
- resp, err = resty.R().SetHeader("Range", "=0").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+ rthdlr.CreateBlobUpload(response, request)
- resp, err = resty.R().SetHeader("Range", "=a").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
- resp, err = resty.R().SetHeader("Range", "").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPost, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ qparm = request.URL.Query()
+ qparm.Add("mount", "a")
+ request.URL.RawQuery = qparm.Encode()
+ response = httptest.NewRecorder()
- resp, err = resty.R().SetHeader("Range", "=").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+ rthdlr.CreateBlobUpload(response, request)
- resp, err = resty.R().SetHeader("Range", "byte=").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusAccepted)
+ })
- resp, err = resty.R().SetHeader("Range", "bytes=").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+ Convey("Get blob upload", func() {
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{})
+ response := httptest.NewRecorder()
- resp, err = resty.R().SetHeader("Range", "byte=-0").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+ rthdlr.GetBlobUpload(response, request)
- resp, err = resty.R().SetHeader("Range", "byte=0").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+ resp := response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- resp, err = resty.R().SetHeader("Range", "octet=-0").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ response = httptest.NewRecorder()
- resp, err = resty.R().SetHeader("Range", "bytes=-0").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+ rthdlr.GetBlobUpload(response, request)
- resp, err = resty.R().SetHeader("Range", "bytes=1-0").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
+ })
- resp, err = resty.R().SetHeader("Range", "bytes=-1-0").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+ Convey("Patch blob upload", func() {
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPatch, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{})
+ response := httptest.NewRecorder()
- resp, err = resty.R().SetHeader("Range", "bytes=-1--0").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+ rthdlr.PatchBlobUpload(response, request)
- resp, err = resty.R().SetHeader("Range", "bytes=1--2").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+ resp := response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- resp, err = resty.R().SetHeader("Range", "bytes=0-a").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ response = httptest.NewRecorder()
- resp, err = resty.R().SetHeader("Range", "bytes=a-10").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+ rthdlr.PatchBlobUpload(response, request)
- resp, err = resty.R().SetHeader("Range", "bytes=a-b").Get(blobLoc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
})
- })
-}
-func TestInjectInterruptedImageManifest(t *testing.T) {
- Convey("Make a new controller", t, func() {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- conf := config.New()
- conf.HTTP.Port = port
+ Convey("Update blob upload", func() {
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{})
+ response := httptest.NewRecorder()
- dir := t.TempDir()
- ctlr := makeController(conf, dir)
+ rthdlr.UpdateBlobUpload(response, request)
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ resp := response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- rthdlr := api.NewRouteHandler(ctlr)
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ response = httptest.NewRecorder()
- Convey("Upload a blob & a config blob; Create an image manifest", func() {
- // create a blob/layer
- resp, err := resty.R().Post(baseURL + "/v2/repotest/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
- loc := test.Location(baseURL, resp)
- So(loc, ShouldNotBeEmpty)
+ rthdlr.UpdateBlobUpload(response, request)
- // since we are not specifying any prefix i.e provided in config while starting server,
- // so it should store repotest to global root dir
- _, err = os.Stat(path.Join(dir, "repotest"))
- So(err, ShouldBeNil)
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- resp, err = resty.R().Get(loc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
- content := []byte("this is a dummy blob")
- digest := godigest.FromBytes(content)
- So(digest, ShouldNotBeNil)
- // monolithic blob upload: success
- resp, err = resty.R().SetQueryParam("digest", digest.String()).
- SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- blobLoc := resp.Header().Get("Location")
- So(blobLoc, ShouldNotBeEmpty)
- So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
- So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty)
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo", "session_id": "bar"})
+ response = httptest.NewRecorder()
- // upload image config blob
- resp, err = resty.R().Post(baseURL + "/v2/repotest/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
- loc = test.Location(baseURL, resp)
- cblob, cdigest := GetRandomImageConfig()
+ rthdlr.UpdateBlobUpload(response, request)
- resp, err = resty.R().
- SetContentLength(true).
- 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.StatusCode(), ShouldEqual, http.StatusCreated)
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
- // create a manifest
- manifest := 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: digest,
- Size: int64(len(content)),
- },
- },
- }
- manifest.SchemaVersion = 2
- content, err = json.Marshal(manifest)
- So(err, ShouldBeNil)
- digest = godigest.FromBytes(content)
- So(digest, ShouldNotBeNil)
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo", "session_id": "bar"})
+ qparm := request.URL.Query()
+ qparm.Add("digest", "a")
+ qparm.Add("digest", "abc")
+ request.URL.RawQuery = qparm.Encode()
+ response = httptest.NewRecorder()
- // Testing router path: @Router /v2/{name}/manifests/{reference} [put]
- Convey("Uploading an image manifest blob (when injected simulates an interrupted image manifest upload)", func() {
- injected := inject.InjectFailure(0)
+ rthdlr.UpdateBlobUpload(response, request)
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, bytes.NewReader(content))
- request = mux.SetURLVars(request, map[string]string{"name": "repotest", "reference": "1.0"})
- request.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
- response := httptest.NewRecorder()
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
+ })
- rthdlr.UpdateManifest(response, request)
+ Convey("Delete blob upload", func() {
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{})
+ response := httptest.NewRecorder()
- resp := response.Result()
- defer resp.Body.Close()
+ rthdlr.DeleteBlobUpload(response, request)
- So(resp, ShouldNotBeNil)
+ resp := response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- if injected {
- So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
- } else {
- So(resp.StatusCode, ShouldEqual, http.StatusCreated)
- }
- })
- })
- })
-}
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodDelete, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ response = httptest.NewRecorder()
-func TestInjectTooManyOpenFiles(t *testing.T) {
- Convey("Make a new controller", t, func() {
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- conf := config.New()
- conf.HTTP.Port = port
+ rthdlr.DeleteBlobUpload(response, request)
- dir := t.TempDir()
- ctlr := makeController(conf, dir)
- conf.Storage.RemoteCache = false
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
+ })
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ Convey("Get referrers", func() {
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{})
+ response := httptest.NewRecorder()
- rthdlr := api.NewRouteHandler(ctlr)
+ rthdlr.GetOrasReferrers(response, request)
- // create a blob/layer
- resp, err := resty.R().Post(baseURL + "/v2/repotest/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
- loc := test.Location(baseURL, resp)
- So(loc, ShouldNotBeEmpty)
+ resp := response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusNotFound)
- // since we are not specifying any prefix i.e provided in config while starting server,
- // so it should store repotest to global root dir
- _, err = os.Stat(path.Join(dir, "repotest"))
- So(err, ShouldBeNil)
+ request, _ = http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "foo"})
+ response = httptest.NewRecorder()
- resp, err = resty.R().Get(loc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
- content := []byte("this is a dummy blob")
- digest := godigest.FromBytes(content)
- So(digest, ShouldNotBeNil)
+ rthdlr.GetOrasReferrers(response, request)
- // monolithic blob upload
- injected := inject.InjectFailure(2)
- if injected {
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, loc, bytes.NewReader(content))
- tokens := strings.Split(loc, "/")
- request = mux.SetURLVars(request, map[string]string{"name": "repotest", "session_id": tokens[len(tokens)-1]})
- q := request.URL.Query()
- q.Add("digest", digest.String())
- request.URL.RawQuery = q.Encode()
- request.Header.Set("Content-Type", "application/octet-stream")
- request.Header.Set("Content-Length", fmt.Sprintf("%d", len(content)))
- response := httptest.NewRecorder()
+ resp = response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusBadRequest)
+ })
+ })
+}
- rthdlr.UpdateBlobUpload(response, request)
+func TestListingTags(t *testing.T) {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ conf := config.New()
+ conf.HTTP.Port = port
- resp := response.Result()
- defer resp.Body.Close()
+ ctlr := makeController(conf, t.TempDir())
+ ctlr.Config.Storage.Commit = true
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
- } else {
- resp, err = resty.R().SetQueryParam("digest", digest.String()).
- SetHeader("Content-Type", "application/octet-stream").
- SetBody(content).Put(loc)
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
- blobLoc := resp.Header().Get("Location")
- So(blobLoc, ShouldNotBeEmpty)
- So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
- So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty)
- }
+ defer cm.StopServer()
- // upload image config blob
- resp, err = resty.R().Post(baseURL + "/v2/repotest/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
- loc = test.Location(baseURL, resp)
- cblob, cdigest := GetRandomImageConfig()
+ rthdlr := api.NewRouteHandler(ctlr)
- resp, err = resty.R().
- SetContentLength(true).
- 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.StatusCode(), ShouldEqual, http.StatusCreated)
+ img := CreateRandomImage()
+ sigTag := fmt.Sprintf("sha256-%s.sig", img.Digest().Encoded())
- // create a manifest
- manifest := ispec.Manifest{
- Config: ispec.Descriptor{
- MediaType: "application/vnd.oci.image.config.v1+json",
- Digest: cdigest,
- Size: int64(len(cblob)),
+ repoName := "test-tags"
+ tagsList := []string{
+ "1", "2", "1.0.0", "new", "2.0.0", sigTag,
+ "2-test", "New", "2.0.0-test", "latest",
+ }
+
+ for _, tag := range tagsList {
+ err := UploadImage(img, baseURL, repoName, tag)
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ // Note empty strings signify the query parameter is not set
+ // There are separate tests for passing the empty string as query parameter
+ testCases := []struct {
+ testCaseName string
+ pageSize string
+ last string
+ expectedTags []string
+ }{
+ {
+ testCaseName: "Test tag soting order with no parameters",
+ pageSize: "",
+ last: "",
+ expectedTags: []string{
+ "1", "1.0.0", "2", "2-test", "2.0.0", "2.0.0-test",
+ "New", "latest", "new", sigTag,
},
- Layers: []ispec.Descriptor{
- {
- MediaType: "application/vnd.oci.image.layer.v1.tar",
- Digest: digest,
- Size: int64(len(content)),
- },
+ },
+ {
+ testCaseName: "Test with the parameter 'n' lower than total number of results",
+ pageSize: "5",
+ last: "",
+ expectedTags: []string{
+ "1", "1.0.0", "2", "2-test", "2.0.0",
},
- }
- manifest.SchemaVersion = 2
- content, err = json.Marshal(manifest)
- So(err, ShouldBeNil)
- digest = godigest.FromBytes(content)
- So(digest, ShouldNotBeNil)
+ },
+ {
+ testCaseName: "Test with the parameter 'n' larger than total number of results",
+ pageSize: "50",
+ last: "",
+ expectedTags: []string{
+ "1", "1.0.0", "2", "2-test", "2.0.0", "2.0.0-test",
+ "New", "latest", "new", sigTag,
+ },
+ },
+ {
+ testCaseName: "Test the parameter 'n' is 0",
+ pageSize: "0",
+ last: "",
+ expectedTags: []string{},
+ },
+ {
+ testCaseName: "Test the parameters 'n' and 'last'",
+ pageSize: "5",
+ last: "2-test",
+ expectedTags: []string{"2.0.0", "2.0.0-test", "New", "latest", "new"},
+ },
+ {
+ testCaseName: "Test the parameters 'n' and 'last' with `n` exceeding total number of results",
+ pageSize: "5",
+ last: "latest",
+ expectedTags: []string{"new", sigTag},
+ },
+ {
+ testCaseName: "Test the parameter 'n' and 'last' being second to last tag as value",
+ pageSize: "2",
+ last: "new",
+ expectedTags: []string{sigTag},
+ },
+ {
+ testCaseName: "Test the parameter 'last' without parameter 'n'",
+ pageSize: "",
+ last: "2",
+ expectedTags: []string{
+ "2-test", "2.0.0", "2.0.0-test",
+ "New", "latest", "new", sigTag,
+ },
+ },
+ {
+ testCaseName: "Test the parameter 'last' with the final tag as value",
+ pageSize: "",
+ last: sigTag,
+ expectedTags: []string{},
+ },
+ {
+ testCaseName: "Test the parameter 'last' with the second to last tag as value",
+ pageSize: "",
+ last: "new",
+ expectedTags: []string{sigTag},
+ },
+ }
- // Testing router path: @Router /v2/{name}/manifests/{reference} [put]
- //nolint:lll // gofumpt conflicts with lll
- Convey("Uploading an image manifest blob (when injected simulates that PutImageManifest failed due to 'too many open files' error)", func() {
- injected := inject.InjectFailure(2)
+ for _, testCase := range testCases {
+ Convey(testCase.testCaseName, t, func() {
+ t.Log("Running " + testCase.testCaseName)
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, bytes.NewReader(content))
- request = mux.SetURLVars(request, map[string]string{"name": "repotest", "reference": "1.0"})
- request.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
- response := httptest.NewRecorder()
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": repoName})
- rthdlr.UpdateManifest(response, request)
+ if testCase.pageSize != "" || testCase.last != "" {
+ qparm := request.URL.Query()
- resp := response.Result()
- So(resp, ShouldNotBeNil)
- defer resp.Body.Close()
+ if testCase.pageSize != "" {
+ qparm.Add("n", testCase.pageSize)
+ }
- if injected {
- So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
- } else {
- So(resp.StatusCode, ShouldEqual, http.StatusCreated)
+ if testCase.last != "" {
+ qparm.Add("last", testCase.last)
+ }
+
+ request.URL.RawQuery = qparm.Encode()
}
- })
- Convey("when injected simulates a `too many open files` error inside PutImageManifest method of img store", func() {
- injected := inject.InjectFailure(2)
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, bytes.NewReader(content))
- request = mux.SetURLVars(request, map[string]string{"name": "repotest", "reference": "1.0"})
- request.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
response := httptest.NewRecorder()
- rthdlr.UpdateManifest(response, request)
+ rthdlr.ListTags(response, request)
resp := response.Result()
defer resp.Body.Close()
-
So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusOK)
- if injected {
- So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
+ var tags common.ImageTags
+ err := json.NewDecoder(resp.Body).Decode(&tags)
+ So(err, ShouldBeNil)
+ So(tags.Tags, ShouldEqual, testCase.expectedTags)
+
+ alltags := tagsList
+ sort.Strings(alltags)
+
+ actualLinkValue := resp.Header.Get("Link")
+ if testCase.pageSize == "0" || testCase.pageSize == "" { //nolint:gocritic
+ So(actualLinkValue, ShouldEqual, "")
+ } else if testCase.expectedTags[len(testCase.expectedTags)-1] == alltags[len(alltags)-1] {
+ So(actualLinkValue, ShouldEqual, "")
} else {
- So(resp.StatusCode, ShouldEqual, http.StatusCreated)
+ expectedLinkValue := fmt.Sprintf("/v2/%s/tags/list?n=%s&last=%s; rel=\"next\"",
+ repoName, testCase.pageSize, tags.Tags[len(tags.Tags)-1],
+ )
+ So(actualLinkValue, ShouldEqual, expectedLinkValue)
}
+
+ t.Log("Finished " + testCase.testCaseName)
})
- Convey("code coverage: error inside PutImageManifest method of img store (unable to marshal JSON)", func() {
- injected := inject.InjectFailure(1)
+ }
+}
- request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, bytes.NewReader(content))
- request = mux.SetURLVars(request, map[string]string{"name": "repotest", "reference": "1.0"})
- request.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
- response := httptest.NewRecorder()
+func TestStorageCommit(t *testing.T) {
+ Convey("Make a new controller", t, func() {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ conf := config.New()
+ conf.HTTP.Port = port
- rthdlr.UpdateManifest(response, request)
+ dir := t.TempDir()
+ ctlr := makeController(conf, dir)
+ ctlr.Config.Storage.Commit = true
- resp := response.Result()
- defer resp.Body.Close()
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- So(resp, ShouldNotBeNil)
+ Convey("Manifests", func() {
+ _, _ = Print("\nManifests")
+
+ cfg, layers, manifest, err := deprecated.GetImageComponents(2) //nolint:staticcheck
+ So(err, ShouldBeNil)
+
+ content := []byte("this is a blob5")
+ digest := godigest.FromBytes(content)
+ So(digest, ShouldNotBeNil)
+ // check a non-existent manifest
+ resp, err := resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
+ SetBody(content).Head(baseURL + "/v2/unknown/manifests/test:1.0")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+
+ repoName := "repo7"
+ err = UploadImage(
+ Image{
+ Config: cfg,
+ Layers: layers,
+ Manifest: manifest,
+ }, baseURL, repoName, "test:1.0")
+ So(err, ShouldBeNil)
- if injected {
- So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
- } else {
- So(resp.StatusCode, ShouldEqual, http.StatusCreated)
- }
- })
+ _, err = os.Stat(path.Join(dir, "repo7"))
+ So(err, ShouldBeNil)
- Convey("when index.json is not in json format", func() {
+ content, err = json.Marshal(manifest)
+ So(err, ShouldBeNil)
+ digest = godigest.FromBytes(content)
+ So(digest, ShouldNotBeNil)
resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
- SetBody(content).Put(baseURL + "/v2/repotest/manifests/v1.0")
+ SetBody(content).Put(baseURL + "/v2/repo7/manifests/test:1.0")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
digestHdr := resp.Header().Get(constants.DistContentDigestKey)
So(digestHdr, ShouldNotBeEmpty)
So(digestHdr, ShouldEqual, digest.String())
- indexFile := path.Join(dir, "repotest", "index.json")
- _, err = os.Stat(indexFile)
+ resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
+ SetBody(content).Put(baseURL + "/v2/repo7/manifests/test:1.0.1")
So(err, ShouldBeNil)
- indexContent := []byte(`not a JSON content`)
- err = os.WriteFile(indexFile, indexContent, 0o600)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ digestHdr = resp.Header().Get(constants.DistContentDigestKey)
+ So(digestHdr, ShouldNotBeEmpty)
+ So(digestHdr, ShouldEqual, digest.String())
+
+ content, err = json.Marshal(manifest)
So(err, ShouldBeNil)
- resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
- SetBody(content).Put(baseURL + "/v2/repotest/manifests/v1.1")
+ err = UploadImage(
+ Image{
+ Config: cfg,
+ Layers: layers,
+ Manifest: manifest,
+ }, baseURL, repoName, "test:1.0.1")
So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
- })
- })
-}
-func TestGCSignaturesAndUntaggedManifestsWithMetaDB(t *testing.T) {
- Convey("Make controller", t, func() {
- Convey("Garbage collect signatures without subject and manifests without tags", func(c C) {
- repoName := "testrepo" //nolint:goconst
- tag := "0.0.1"
+ cfg, layers, manifest, err = deprecated.GetImageComponents(1) //nolint:staticcheck
+ So(err, ShouldBeNil)
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- conf := config.New()
- conf.HTTP.Port = port
+ err = UploadImage(
+ Image{
+ Config: cfg,
+ Layers: layers,
+ Manifest: manifest,
+ }, baseURL, repoName, "test:2.0")
+ So(err, ShouldBeNil)
- value := true
- searchConfig := &extconf.SearchConfig{
- BaseConfig: extconf.BaseConfig{Enable: &value},
- }
+ resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
+ SetBody(content).Put(baseURL + "/v2/repo7/manifests/test:2.0")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ digestHdr = resp.Header().Get(constants.DistContentDigestKey)
+ So(digestHdr, ShouldNotBeEmpty)
+ So(digestHdr, ShouldEqual, digest.String())
- // added search extensions so that metaDB is initialized and its tested in GC logic
- conf.Extensions = &extconf.ExtensionConfig{
- Search: searchConfig,
- }
+ // check/get by tag
+ resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/test:1.0")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
+ resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/test:1.0")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ So(resp.Body(), ShouldNotBeEmpty)
+ // check/get by reference
+ resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/" + digest.String())
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
+ resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/" + digest.String())
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ So(resp.Body(), ShouldNotBeEmpty)
- ctlr := makeController(conf, t.TempDir())
+ // delete manifest by tag should pass
+ resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/test:1.0")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
+ // delete manifest by digest (1.0 deleted but 1.0.1 has same reference)
+ resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String())
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
+ // delete manifest by digest
+ resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String())
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ // delete again should fail
+ resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String())
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- dir := t.TempDir()
- ctlr.Config.Storage.RootDirectory = dir
- ctlr.Config.Storage.GC = true
- ctlr.Config.Storage.GCDelay = 1 * time.Millisecond
- ctlr.Config.Storage.UntaggedImageRetentionDelay = 1 * time.Millisecond
+ // check/get by tag
+ resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/test:1.0")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/test:1.0")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ So(resp.Body(), ShouldNotBeEmpty)
+ resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/test:2.0")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/test:2.0")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ So(resp.Body(), ShouldNotBeEmpty)
+ // check/get by reference
+ resp, err = resty.R().Head(baseURL + "/v2/repo7/manifests/" + digest.String())
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ resp, err = resty.R().Get(baseURL + "/v2/repo7/manifests/" + digest.String())
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ So(resp.Body(), ShouldNotBeEmpty)
+ })
+ })
+}
- ctlr.Config.Storage.Dedupe = false
+func TestManifestImageIndex(t *testing.T) {
+ Convey("Make a new controller", t, func() {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ conf := config.New()
+ conf.HTTP.Port = port
- cm := test.NewControllerManager(ctlr)
- cm.StartServer()
- cm.WaitServerToBeReady(port)
- defer cm.StopServer()
+ dir := t.TempDir()
+ ctlr := makeController(conf, dir)
- img := CreateDefaultImage()
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- err := UploadImage(img, baseURL, repoName, tag)
- So(err, ShouldBeNil)
+ rthdlr := api.NewRouteHandler(ctlr)
- gc := gc.NewGarbageCollect(ctlr.StoreController.DefaultStore, ctlr.MetaDB,
- gc.Options{
- Referrers: ctlr.Config.Storage.GCReferrers,
- Delay: ctlr.Config.Storage.GCDelay,
- RetentionDelay: ctlr.Config.Storage.UntaggedImageRetentionDelay,
- },
- ctlr.Log)
+ cfg, layers, manifest, err := deprecated.GetImageComponents(2) //nolint:staticcheck
+ So(err, ShouldBeNil)
- resp, err := resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, tag))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- digest := godigest.FromBytes(resp.Body())
- So(digest, ShouldNotBeEmpty)
+ content := []byte("this is a blob1")
+ digest := godigest.FromBytes(content)
+ So(digest, ShouldNotBeNil)
- cwd, err := os.Getwd()
- So(err, ShouldBeNil)
- defer func() { _ = os.Chdir(cwd) }()
- tdir := t.TempDir()
- _ = os.Chdir(tdir)
+ // check a non-existent manifest
+ resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody(content).Head(baseURL + "/v2/unknown/manifests/test:1.0")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- // generate a keypair
- os.Setenv("COSIGN_PASSWORD", "")
- err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil)
- So(err, ShouldBeNil)
+ repoName := "index"
+ err = UploadImage(
+ Image{
+ Config: cfg,
+ Layers: layers,
+ Manifest: manifest,
+ }, baseURL, repoName, "test:1.0")
+ So(err, ShouldBeNil)
- image := fmt.Sprintf("localhost:%s/%s@%s", port, repoName, digest.String())
+ _, err = os.Stat(path.Join(dir, "index"))
+ So(err, ShouldBeNil)
- annotations := []string{fmt.Sprintf("tag=%s", tag)}
+ content, err = json.Marshal(manifest)
+ So(err, ShouldBeNil)
+ digest = godigest.FromBytes(content)
+ So(digest, ShouldNotBeNil)
+ m1content := content
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody(content).Put(baseURL + "/v2/index/manifests/test:1.0")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ digestHdr := resp.Header().Get(constants.DistContentDigestKey)
+ So(digestHdr, ShouldNotBeEmpty)
+ So(digestHdr, ShouldEqual, digest.String())
- // sign the image
- err = sign.SignCmd(&options.RootOptions{Verbose: true, Timeout: 1 * time.Minute},
- options.KeyOpts{KeyRef: path.Join(tdir, "cosign.key"), PassFunc: generate.GetPass},
- options.SignOptions{
- Registry: options.RegistryOptions{AllowInsecure: true},
- AnnotationOptions: options.AnnotationOptions{Annotations: annotations},
- Upload: true,
- },
- []string{image})
+ // create another manifest but upload using its sha256 reference
- So(err, ShouldBeNil)
+ // upload image config blob
+ resp, err = resty.R().Post(baseURL + "/v2/index/blobs/uploads/")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
- signature.NotationPathLock.Lock()
- defer signature.NotationPathLock.Unlock()
+ img := CreateRandomImage()
- signature.LoadNotationPath(tdir)
+ err = UploadImage(img, baseURL, repoName, img.DigestStr())
+ So(err, ShouldBeNil)
- // generate a keypair
- err = signature.GenerateNotationCerts(tdir, "good")
- So(err, ShouldBeNil)
+ content = img.ManifestDescriptor.Data
+ digest = img.ManifestDescriptor.Digest
- // sign the image
- err = signature.SignWithNotation("good", image, tdir)
- So(err, ShouldBeNil)
+ m2dgst := digest
+ m2size := len(content)
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody(content).Put(baseURL + fmt.Sprintf("/v2/index/manifests/%s", digest))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ digestHdr = resp.Header().Get(constants.DistContentDigestKey)
+ So(digestHdr, ShouldNotBeEmpty)
+ So(digestHdr, ShouldEqual, digest.String())
- // get cosign signature manifest
- cosignTag := strings.Replace(digest.String(), ":", "-", 1) + "." + remote.SignatureTagSuffix
+ Convey("Image index", func() {
+ img := CreateRandomImage()
- resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, cosignTag))
+ err = UploadImage(img, baseURL, repoName, img.DigestStr())
So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- cosignDigest := resp.Header().Get(constants.DistContentDigestKey)
- So(cosignDigest, ShouldNotBeEmpty)
+ content := img.ManifestDescriptor.Data
+ digest = img.ManifestDescriptor.Digest
- // get notation signature manifest
- resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
- fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String()))
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody(content).Put(baseURL + fmt.Sprintf("/v2/index/manifests/%s", digest.String()))
So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ digestHdr := resp.Header().Get(constants.DistContentDigestKey)
+ So(digestHdr, ShouldNotBeEmpty)
+ So(digestHdr, ShouldEqual, digest.String())
var index ispec.Index
+ index.SchemaVersion = 2
+ index.Manifests = []ispec.Descriptor{
+ {
+ MediaType: ispec.MediaTypeImageManifest,
+ Digest: digest,
+ Size: int64(len(content)),
+ },
+ {
+ MediaType: ispec.MediaTypeImageManifest,
+ Digest: m2dgst,
+ Size: int64(m2size),
+ },
+ }
- err = json.Unmarshal(resp.Body(), &index)
+ content, err = json.Marshal(index)
So(err, ShouldBeNil)
- So(len(index.Manifests), ShouldEqual, 1)
-
- // shouldn't do anything
- err = gc.CleanRepo(repoName)
+ digest = godigest.FromBytes(content)
+ So(digest, ShouldNotBeNil)
+ index1dgst := digest
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ SetBody(content).Put(baseURL + "/v2/index/manifests/test:index1")
So(err, ShouldBeNil)
-
- // make sure both signatures are stored in repodb
- repoMeta, err := ctlr.MetaDB.GetRepoMeta(repoName)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ digestHdr = resp.Header().Get(constants.DistContentDigestKey)
+ So(digestHdr, ShouldNotBeEmpty)
+ So(digestHdr, ShouldEqual, digest.String())
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ Get(baseURL + "/v2/index/manifests/test:index1")
So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ So(resp.Body(), ShouldNotBeEmpty)
+ So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
- sigMeta := repoMeta.Signatures[digest.String()]
- So(len(sigMeta[storage.CosignType]), ShouldEqual, 1)
- So(len(sigMeta[storage.NotationType]), ShouldEqual, 1)
- So(sigMeta[storage.CosignType][0].SignatureManifestDigest, ShouldEqual, cosignDigest)
- So(sigMeta[storage.NotationType][0].SignatureManifestDigest, ShouldEqual, index.Manifests[0].Digest.String())
-
- Convey("Trigger gcNotationSignatures() error", func() {
- var refs ispec.Index
- err = json.Unmarshal(resp.Body(), &refs)
+ img = CreateRandomImage()
- err := os.Chmod(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded()), 0o000)
- So(err, ShouldBeNil)
+ err = UploadImage(img, baseURL, repoName, img.DigestStr())
+ So(err, ShouldBeNil)
- // trigger gc
- img := CreateRandomImage()
+ content = img.ManifestDescriptor.Data
+ digest = img.ManifestDescriptor.Digest
- err = UploadImage(img, baseURL, repoName, img.DigestStr())
- So(err, ShouldBeNil)
+ m4dgst := digest
+ m4size := len(content)
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody(content).Put(baseURL + fmt.Sprintf("/v2/index/manifests/%s", digest.String()))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ digestHdr = resp.Header().Get(constants.DistContentDigestKey)
+ So(digestHdr, ShouldNotBeEmpty)
+ So(digestHdr, ShouldEqual, digest.String())
- err = gc.CleanRepo(repoName)
- So(err, ShouldNotBeNil)
+ index.SchemaVersion = 2
+ index.Manifests = []ispec.Descriptor{
+ {
+ MediaType: ispec.MediaTypeImageManifest,
+ Digest: digest,
+ Size: int64(len(content)),
+ },
+ {
+ MediaType: ispec.MediaTypeImageManifest,
+ Digest: m2dgst,
+ Size: int64(m2size),
+ },
+ }
- err = os.Chmod(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded()), 0o755)
- So(err, ShouldBeNil)
+ content, err = json.Marshal(index)
+ So(err, ShouldBeNil)
+ digest = godigest.FromBytes(content)
+ So(digest, ShouldNotBeNil)
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ SetBody(content).Put(baseURL + "/v2/index/manifests/test:index2")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ digestHdr = resp.Header().Get(constants.DistContentDigestKey)
+ So(digestHdr, ShouldNotBeEmpty)
+ So(digestHdr, ShouldEqual, digest.String())
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ Get(baseURL + "/v2/index/manifests/test:index2")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ So(resp.Body(), ShouldNotBeEmpty)
+ So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
- content, err := os.ReadFile(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded()))
- So(err, ShouldBeNil)
- err = os.WriteFile(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded()), []byte("corrupt"), 0o600) //nolint:lll
- So(err, ShouldBeNil)
+ Convey("List tags", func() {
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, baseURL, nil)
+ request = mux.SetURLVars(request, map[string]string{"name": "index"})
+ response := httptest.NewRecorder()
- err = UploadImage(img, baseURL, repoName, tag)
- So(err, ShouldBeNil)
+ rthdlr.ListTags(response, request)
- err = gc.CleanRepo(repoName)
- So(err, ShouldNotBeNil)
+ resp := response.Result()
+ defer resp.Body.Close()
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusOK)
- err = os.WriteFile(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded()), content, 0o600)
+ var tags common.ImageTags
+ err = json.NewDecoder(resp.Body).Decode(&tags)
So(err, ShouldBeNil)
+ So(len(tags.Tags), ShouldEqual, 3)
+ So(tags.Tags, ShouldContain, "test:1.0")
+ So(tags.Tags, ShouldContain, "test:index1")
+ So(tags.Tags, ShouldContain, "test:index2")
})
- Convey("Overwrite original image, signatures should be garbage-collected", func() {
- // push an image without tag
- cfg, layers, manifest, err := deprecated.GetImageComponents(2) //nolint:staticcheck
- So(err, ShouldBeNil)
-
- manifestBuf, err := json.Marshal(manifest)
- So(err, ShouldBeNil)
- untaggedManifestDigest := godigest.FromBytes(manifestBuf)
+ Convey("Another index with same manifest", func() {
+ var index ispec.Index
+ index.SchemaVersion = 2
+ index.Manifests = []ispec.Descriptor{
+ {
+ MediaType: ispec.MediaTypeImageManifest,
+ Digest: m4dgst,
+ Size: int64(m4size),
+ },
+ }
- err = UploadImage(
- Image{
- Config: cfg,
- Layers: layers,
- Manifest: manifest,
- }, baseURL, repoName, untaggedManifestDigest.String())
+ content, err = json.Marshal(index)
So(err, ShouldBeNil)
-
- // make sure repoDB reference was added
- repoMeta, err := ctlr.MetaDB.GetRepoMeta(repoName)
+ digest = godigest.FromBytes(content)
+ So(digest, ShouldNotBeNil)
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ SetBody(content).Put(baseURL + "/v2/index/manifests/test:index3")
So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ digestHdr = resp.Header().Get(constants.DistContentDigestKey)
+ So(digestHdr, ShouldNotBeEmpty)
+ So(digestHdr, ShouldEqual, digest.String())
+ })
- _, ok := repoMeta.Referrers[untaggedManifestDigest.String()]
- So(ok, ShouldBeTrue)
- _, ok = repoMeta.Signatures[untaggedManifestDigest.String()]
- So(ok, ShouldBeTrue)
- _, ok = repoMeta.Statistics[untaggedManifestDigest.String()]
- So(ok, ShouldBeTrue)
-
- // overwrite image so that signatures will get invalidated and gc'ed
- cfg, layers, manifest, err = deprecated.GetImageComponents(3) //nolint:staticcheck
- So(err, ShouldBeNil)
+ Convey("Another index using digest with same manifest", func() {
+ var index ispec.Index
+ index.SchemaVersion = 2
+ index.Manifests = []ispec.Descriptor{
+ {
+ MediaType: ispec.MediaTypeImageManifest,
+ Digest: m4dgst,
+ Size: int64(m4size),
+ },
+ }
- err = UploadImage(
- Image{
- Config: cfg,
- Layers: layers,
- Manifest: manifest,
- }, baseURL, repoName, tag)
+ content, err = json.Marshal(index)
So(err, ShouldBeNil)
-
- manifestBuf, err = json.Marshal(manifest)
+ digest = godigest.FromBytes(content)
+ So(digest, ShouldNotBeNil)
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ SetBody(content).Put(baseURL + fmt.Sprintf("/v2/index/manifests/%s", digest))
So(err, ShouldBeNil)
- newManifestDigest := godigest.FromBytes(manifestBuf)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ digestHdr = resp.Header().Get(constants.DistContentDigestKey)
+ So(digestHdr, ShouldNotBeEmpty)
+ So(digestHdr, ShouldEqual, digest.String())
+ })
- err = gc.CleanRepo(repoName)
+ Convey("Deleting manifest contained by a multiarch image should not be allowed", func() {
+ resp, err = resty.R().Delete(baseURL + fmt.Sprintf("/v2/index/manifests/%s", m2dgst.String()))
So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusMethodNotAllowed)
+ })
- // make sure both signatures are removed from repodb and repo reference for untagged is removed
- repoMeta, err = ctlr.MetaDB.GetRepoMeta(repoName)
+ Convey("Deleting an image index", func() {
+ // delete manifest by tag should pass
+ resp, err = resty.R().Delete(baseURL + "/v2/index/manifests/test:index3")
So(err, ShouldBeNil)
-
- sigMeta := repoMeta.Signatures[digest.String()]
- So(len(sigMeta[storage.CosignType]), ShouldEqual, 0)
- So(len(sigMeta[storage.NotationType]), ShouldEqual, 0)
-
- _, ok = repoMeta.Referrers[untaggedManifestDigest.String()]
- So(ok, ShouldBeFalse)
- _, ok = repoMeta.Signatures[untaggedManifestDigest.String()]
- So(ok, ShouldBeFalse)
- _, ok = repoMeta.Statistics[untaggedManifestDigest.String()]
- So(ok, ShouldBeFalse)
-
- // both signatures should be gc'ed
- resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, cosignTag))
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ Get(baseURL + "/v2/index/manifests/test:index3")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
-
- resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
- fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String()))
+ So(resp.Body(), ShouldNotBeEmpty)
+ resp, err = resty.R().Delete(baseURL + "/v2/index/manifests/test:index1")
+ So(err, ShouldBeNil)
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ Get(baseURL + "/v2/index/manifests/test:index1")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ So(resp.Body(), ShouldNotBeEmpty)
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ Get(baseURL + "/v2/index/manifests/test:index2")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ So(resp.Body(), ShouldNotBeEmpty)
+ So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
+ })
- err = json.Unmarshal(resp.Body(), &index)
+ Convey("Deleting an image index by digest", func() {
+ // delete manifest by tag should pass
+ resp, err = resty.R().Delete(baseURL + "/v2/index/manifests/test:index3")
So(err, ShouldBeNil)
- So(len(index.Manifests), ShouldEqual, 0)
-
- resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
- fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, newManifestDigest.String()))
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ Get(baseURL + "/v2/index/manifests/test:index3")
So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- err = json.Unmarshal(resp.Body(), &index)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ So(resp.Body(), ShouldNotBeEmpty)
+ resp, err = resty.R().Delete(baseURL + fmt.Sprintf("/v2/index/manifests/%s", index1dgst))
So(err, ShouldBeNil)
- So(len(index.Manifests), ShouldEqual, 0)
-
- // untagged image should also be gc'ed
- resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, untaggedManifestDigest))
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ Get(baseURL + "/v2/index/manifests/test:index1")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ So(resp.Body(), ShouldNotBeEmpty)
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ Get(baseURL + "/v2/index/manifests/test:index2")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ So(resp.Body(), ShouldNotBeEmpty)
+ So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
})
- })
-
- Convey("Do not gc manifests which are part of a multiarch image", func(c C) {
- repoName := "testrepo" //nolint:goconst
- tag := "0.0.1"
-
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- conf := config.New()
- conf.HTTP.Port = port
-
- ctlr := makeController(conf, t.TempDir())
-
- dir := t.TempDir()
- ctlr.Config.Storage.RootDirectory = dir
- ctlr.Config.Storage.GC = true
- ctlr.Config.Storage.GCDelay = 1 * time.Second
- ctlr.Config.Storage.UntaggedImageRetentionDelay = 1 * time.Second
-
- err := WriteImageToFileSystem(CreateDefaultImage(), repoName, tag,
- ociutils.GetDefaultStoreController(dir, ctlr.Log))
- So(err, ShouldBeNil)
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
-
- gc := gc.NewGarbageCollect(ctlr.StoreController.DefaultStore, ctlr.MetaDB,
- gc.Options{
- Referrers: ctlr.Config.Storage.GCReferrers,
- Delay: ctlr.Config.Storage.GCDelay,
- RetentionDelay: ctlr.Config.Storage.UntaggedImageRetentionDelay,
- }, ctlr.Log)
-
- resp, err := resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, tag))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- digest := godigest.FromBytes(resp.Body())
- So(digest, ShouldNotBeEmpty)
-
- // push an image index and make sure manifests contained by it are not gc'ed
- // create an image index on upstream
- var index ispec.Index
- index.SchemaVersion = 2
- index.MediaType = ispec.MediaTypeImageIndex
- // upload multiple manifests
- for i := 0; i < 4; i++ {
- config, layers, manifest, err := deprecated.GetImageComponents(1000 + i) //nolint:staticcheck
- So(err, ShouldBeNil)
+ Convey("Update an index tag with different manifest", func() {
+ img := CreateRandomImage()
- manifestContent, err := json.Marshal(manifest)
+ err = UploadImage(img, baseURL, repoName, img.DigestStr())
So(err, ShouldBeNil)
- manifestDigest := godigest.FromBytes(manifestContent)
+ content = img.ManifestDescriptor.Data
+ digest = img.ManifestDescriptor.Digest
- err = UploadImage(
- Image{
- Manifest: manifest,
- Config: config,
- Layers: layers,
- }, baseURL, repoName, manifestDigest.String())
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody(content).Put(baseURL + fmt.Sprintf("/v2/index/manifests/%s", digest))
So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ digestHdr = resp.Header().Get(constants.DistContentDigestKey)
+ So(digestHdr, ShouldNotBeEmpty)
+ So(digestHdr, ShouldEqual, digest.String())
- index.Manifests = append(index.Manifests, ispec.Descriptor{
- Digest: manifestDigest,
- MediaType: ispec.MediaTypeImageManifest,
- Size: int64(len(manifestContent)),
- })
- }
-
- content, err := json.Marshal(index)
- So(err, ShouldBeNil)
- indexDigest := godigest.FromBytes(content)
- So(indexDigest, ShouldNotBeNil)
-
- time.Sleep(1 * time.Second)
- // upload image index
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- SetBody(content).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/latest", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
-
- err = gc.CleanRepo(repoName)
- So(err, ShouldBeNil)
-
- resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
- Get(baseURL + fmt.Sprintf("/v2/%s/manifests/latest", repoName))
- So(err, ShouldBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- So(resp.Body(), ShouldNotBeEmpty)
- So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
+ index.SchemaVersion = 2
+ index.Manifests = []ispec.Descriptor{
+ {
+ MediaType: ispec.MediaTypeImageManifest,
+ Digest: digest,
+ Size: int64(len(content)),
+ },
+ }
- // make sure manifests which are part of image index are not gc'ed
- for _, manifest := range index.Manifests {
- resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, manifest.Digest.String()))
+ content, err = json.Marshal(index)
+ So(err, ShouldBeNil)
+ digest = godigest.FromBytes(content)
+ So(digest, ShouldNotBeNil)
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ SetBody(content).Put(baseURL + "/v2/index/manifests/test:index1")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ digestHdr = resp.Header().Get(constants.DistContentDigestKey)
+ So(digestHdr, ShouldNotBeEmpty)
+ So(digestHdr, ShouldEqual, digest.String())
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ Get(baseURL + "/v2/index/manifests/test:index1")
So(err, ShouldBeNil)
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- }
- })
- })
-}
-
-func TestPeriodicGC(t *testing.T) {
- Convey("Periodic gc enabled for default store", t, func() {
- repoName := "testrepo" //nolint:goconst
-
- port := test.GetFreePort()
- conf := config.New()
- conf.HTTP.Port = port
- conf.Storage.RemoteCache = false
+ So(resp.Body(), ShouldNotBeEmpty)
+ So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
- logFile, err := os.CreateTemp("", "zot-log*.txt")
- So(err, ShouldBeNil)
- conf.Log.Output = logFile.Name()
- defer os.Remove(logFile.Name()) // clean up
+ // delete manifest by tag should pass
+ resp, err = resty.R().Delete(baseURL + "/v2/index/manifests/test:index1")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ Get(baseURL + "/v2/index/manifests/test:index1")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ So(resp.Body(), ShouldNotBeEmpty)
+ })
- ctlr := api.NewController(conf)
- dir := t.TempDir()
- ctlr.Config.Storage.RootDirectory = dir
- ctlr.Config.Storage.Dedupe = false
- ctlr.Config.Storage.GC = true
- ctlr.Config.Storage.GCInterval = 1 * time.Hour
- ctlr.Config.Storage.GCDelay = 1 * time.Second
+ Convey("Negative test cases", func() {
+ Convey("Delete index", func() {
+ err = os.Remove(path.Join(dir, "index", "blobs", index1dgst.Algorithm().String(), index1dgst.Encoded()))
+ So(err, ShouldBeNil)
+ resp, err = resty.R().Delete(baseURL + fmt.Sprintf("/v2/index/manifests/%s", index1dgst))
+ So(err, ShouldBeNil)
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ Get(baseURL + "/v2/index/manifests/test:index1")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ So(resp.Body(), ShouldNotBeEmpty)
+ })
- err = WriteImageToFileSystem(CreateDefaultImage(), repoName, "0.0.1",
- ociutils.GetDefaultStoreController(dir, ctlr.Log))
- So(err, ShouldBeNil)
+ Convey("Corrupt index", func() {
+ err = os.WriteFile(path.Join(dir, "index", "blobs", index1dgst.Algorithm().String(), index1dgst.Encoded()),
+ []byte("deadbeef"), storageConstants.DefaultFilePerms)
+ So(err, ShouldBeNil)
+ resp, err = resty.R().Delete(baseURL + fmt.Sprintf("/v2/index/manifests/%s", index1dgst))
+ So(err, ShouldBeNil)
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ Get(baseURL + "/v2/index/manifests/test:index1")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
+ So(resp.Body(), ShouldBeEmpty)
+ })
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ Convey("Change media-type", func() {
+ // previously a manifest, try writing an image index
+ var index ispec.Index
+ index.SchemaVersion = 2
+ index.Manifests = []ispec.Descriptor{
+ {
+ MediaType: ispec.MediaTypeImageIndex,
+ Digest: m4dgst,
+ Size: int64(m4size),
+ },
+ }
- time.Sleep(5000 * time.Millisecond)
+ content, err = json.Marshal(index)
+ So(err, ShouldBeNil)
+ digest = godigest.FromBytes(content)
+ So(digest, ShouldNotBeNil)
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ SetBody(content).Put(baseURL + "/v2/index/manifests/test:1.0")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
- data, err := os.ReadFile(logFile.Name())
- So(err, ShouldBeNil)
- So(string(data), ShouldContainSubstring,
- "\"GC\":true,\"Commit\":false,\"GCDelay\":1000000000,\"GCInterval\":3600000000000")
- So(string(data), ShouldContainSubstring,
- fmt.Sprintf("executing GC of orphaned blobs for %s", path.Join(ctlr.StoreController.DefaultStore.RootDir(), repoName))) //nolint:lll
- So(string(data), ShouldContainSubstring,
- fmt.Sprintf("GC successfully completed for %s", path.Join(ctlr.StoreController.DefaultStore.RootDir(), repoName))) //nolint:lll
+ // previously an image index, try writing a manifest
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody(m1content).Put(baseURL + "/v2/index/manifests/test:index1")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest)
+ })
+ })
+ })
})
+}
- Convey("Periodic GC enabled for substore", t, func() {
+func TestManifestCollision(t *testing.T) {
+ Convey("Make a new controller", t, func() {
port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
- logFile, err := os.CreateTemp("", "zot-log*.txt")
- So(err, ShouldBeNil)
- conf.Log.Output = logFile.Name()
- defer os.Remove(logFile.Name()) // clean up
-
dir := t.TempDir()
ctlr := makeController(conf, dir)
- subDir := t.TempDir()
-
- subPaths := make(map[string]config.StorageConfig)
- subPaths["/a"] = config.StorageConfig{
- RootDirectory: subDir, GC: true, GCDelay: 1 * time.Second,
- UntaggedImageRetentionDelay: 1 * time.Second, GCInterval: 24 * time.Hour, RemoteCache: false, Dedupe: false,
- } //nolint:lll // gofumpt conflicts with lll
- ctlr.Config.Storage.Dedupe = false
- ctlr.Config.Storage.SubPaths = subPaths
+ conf.HTTP.AccessControl = &config.AccessControlConfig{
+ Repositories: config.Repositories{
+ AuthorizationAllRepos: config.PolicyGroup{
+ AnonymousPolicy: []string{
+ constants.ReadPermission,
+ constants.CreatePermission,
+ constants.DeletePermission,
+ constants.DetectManifestCollisionPermission,
+ },
+ },
+ },
+ }
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
- data, err := os.ReadFile(logFile.Name())
+ cfg, layers, manifest, err := deprecated.GetImageComponents(2) //nolint:staticcheck
So(err, ShouldBeNil)
- // periodic GC is enabled by default for default store with a default interval
- So(string(data), ShouldContainSubstring,
- "\"GCDelay\":3600000000000,\"GCInterval\":3600000000000,\"")
- // periodic GC is enabled for sub store
- So(string(data), ShouldContainSubstring,
- fmt.Sprintf("\"SubPaths\":{\"/a\":{\"RootDirectory\":\"%s\",\"Dedupe\":false,\"RemoteCache\":false,\"GC\":true,\"Commit\":false,\"GCDelay\":1000000000,\"GCInterval\":86400000000000", subDir)) //nolint:lll // gofumpt conflicts with lll
- })
-
- Convey("Periodic gc error", t, func() {
- repoName := "testrepo" //nolint:goconst
- port := test.GetFreePort()
- conf := config.New()
- conf.HTTP.Port = port
- conf.Storage.RemoteCache = false
+ err = UploadImage(
+ Image{
+ Config: cfg,
+ Layers: layers,
+ Manifest: manifest,
+ }, baseURL, "index", "test:1.0")
+ So(err, ShouldBeNil)
- logFile, err := os.CreateTemp("", "zot-log*.txt")
+ _, err = os.Stat(path.Join(dir, "index"))
So(err, ShouldBeNil)
- conf.Log.Output = logFile.Name()
- defer os.Remove(logFile.Name()) // clean up
- ctlr := api.NewController(conf)
- dir := t.TempDir()
- ctlr.Config.Storage.RootDirectory = dir
- ctlr.Config.Storage.Dedupe = false
+ content := []byte("this is a blob1")
+ digest := godigest.FromBytes(content)
+ So(digest, ShouldNotBeNil)
- ctlr.Config.Storage.GC = true
- ctlr.Config.Storage.GCInterval = 1 * time.Hour
- ctlr.Config.Storage.GCDelay = 1 * time.Second
+ // check a non-existent manifest
+ resp, err := resty.R().SetHeader("Content-Type", ispec.MediaTypeImageManifest).
+ SetBody(content).Head(baseURL + "/v2/unknown/manifests/test:1.0")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- err = WriteImageToFileSystem(CreateDefaultImage(), repoName, "0.0.1",
- ociutils.GetDefaultStoreController(dir, ctlr.Log))
+ content, err = json.Marshal(manifest)
So(err, ShouldBeNil)
+ digest = godigest.FromBytes(content)
+ So(digest, ShouldNotBeNil)
- So(os.Chmod(dir, 0o000), ShouldBeNil)
+ err = UploadImage(
+ Image{
+ Config: cfg,
+ Layers: layers,
+ Manifest: manifest,
+ }, baseURL, "index", "test:2.0")
+ So(err, ShouldBeNil)
- defer func() {
- So(os.Chmod(dir, 0o755), ShouldBeNil)
- }()
+ // Deletion should fail if using digest
+ resp, err = resty.R().Delete(baseURL + "/v2/index/manifests/" + digest.String())
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusConflict)
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ // remove detectManifestCollision action from ** (all repos)
+ repoPolicy := conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos]
+ repoPolicy.AnonymousPolicy = []string{"read", "delete"}
+ conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos] = repoPolicy
- time.Sleep(5000 * time.Millisecond)
+ resp, err = resty.R().Delete(baseURL + "/v2/index/manifests/" + digest.String())
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
- data, err := os.ReadFile(logFile.Name())
+ resp, err = resty.R().Get(baseURL + "/v2/index/manifests/test:1.0")
So(err, ShouldBeNil)
- So(string(data), ShouldContainSubstring,
- "\"GC\":true,\"Commit\":false,\"GCDelay\":1000000000,\"GCInterval\":3600000000000")
- So(string(data), ShouldContainSubstring, "failure walking storage root-dir") //nolint:lll
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+
+ resp, err = resty.R().Get(baseURL + "/v2/index/manifests/test:2.0")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
})
}
-func TestSearchRoutes(t *testing.T) {
- Convey("Upload image for test", t, func(c C) {
+func TestPullRange(t *testing.T) {
+ Convey("Make a new controller", t, func() {
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
- tempDir := t.TempDir()
- ctlr := makeController(conf, tempDir)
- cm := test.NewControllerManager(ctlr)
+ dir := t.TempDir()
+ ctlr := makeController(conf, dir)
+ cm := test.NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
- repoName := "testrepo" //nolint:goconst
- inaccessibleRepo := "inaccessible"
-
- cfg, layers, manifest, err := deprecated.GetImageComponents(10000) //nolint:staticcheck
+ // create a blob/layer
+ resp, err := resty.R().Post(baseURL + "/v2/index/blobs/uploads/")
So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
+ loc := test.Location(baseURL, resp)
+ So(loc, ShouldNotBeEmpty)
- err = UploadImage(
- Image{
- Config: cfg,
- Layers: layers,
- Manifest: manifest,
- }, baseURL, repoName, "latest")
-
+ // since we are not specifying any prefix i.e provided in config while starting server,
+ // so it should store index1 to global root dir
+ _, err = os.Stat(path.Join(dir, "index"))
So(err, ShouldBeNil)
- // data for the inaccessible repo
- cfg, layers, manifest, err = deprecated.GetImageComponents(10000) //nolint:staticcheck
+ resp, err = resty.R().Get(loc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
+ content := []byte("0123456789")
+ digest := godigest.FromBytes(content)
+ So(digest, ShouldNotBeNil)
+ // monolithic blob upload: success
+ resp, err = resty.R().SetQueryParam("digest", digest.String()).
+ SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc)
So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ blobLoc := resp.Header().Get("Location")
+ So(blobLoc, ShouldNotBeEmpty)
+ So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
+ So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty)
+ blobLoc = baseURL + blobLoc
- err = UploadImage(
- Image{
- Config: cfg,
- Layers: layers,
- Manifest: manifest,
- }, baseURL, inaccessibleRepo, "latest")
+ Convey("Range is supported using 'bytes'", func() {
+ resp, err = resty.R().Head(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.Header().Get("Accept-Ranges"), ShouldEqual, "bytes")
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ })
- So(err, ShouldBeNil)
+ Convey("Get a range of bytes", func() {
+ resp, err = resty.R().SetHeader("Range", "bytes=0-").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusPartialContent)
+ So(resp.Header().Get("Content-Length"), ShouldEqual, fmt.Sprintf("%d", len(content)))
+ So(resp.Body(), ShouldResemble, content)
- Convey("GlobalSearch with authz enabled", func(c C) {
- conf := config.New()
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
+ resp, err = resty.R().SetHeader("Range", "bytes=0-100").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusPartialContent)
+ So(resp.Header().Get("Content-Length"), ShouldEqual, fmt.Sprintf("%d", len(content)))
+ So(resp.Body(), ShouldResemble, content)
+
+ resp, err = resty.R().SetHeader("Range", "bytes=0-10").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusPartialContent)
+ So(resp.Header().Get("Content-Length"), ShouldEqual, fmt.Sprintf("%d", len(content)))
+ So(resp.Body(), ShouldResemble, content)
+
+ resp, err = resty.R().SetHeader("Range", "bytes=0-0").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusPartialContent)
+ So(resp.Header().Get("Content-Length"), ShouldEqual, "1")
+ So(resp.Body(), ShouldResemble, content[0:1])
+
+ resp, err = resty.R().SetHeader("Range", "bytes=0-1").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusPartialContent)
+ So(resp.Header().Get("Content-Length"), ShouldEqual, "2")
+ So(resp.Body(), ShouldResemble, content[0:2])
+
+ resp, err = resty.R().SetHeader("Range", "bytes=2-3").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusPartialContent)
+ So(resp.Header().Get("Content-Length"), ShouldEqual, "2")
+ So(resp.Body(), ShouldResemble, content[2:4])
+ })
+
+ Convey("Negative cases", func() {
+ resp, err = resty.R().SetHeader("Range", "=0").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+
+ resp, err = resty.R().SetHeader("Range", "=a").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+
+ resp, err = resty.R().SetHeader("Range", "").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+
+ resp, err = resty.R().SetHeader("Range", "=").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+
+ resp, err = resty.R().SetHeader("Range", "byte=").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+
+ resp, err = resty.R().SetHeader("Range", "bytes=").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+
+ resp, err = resty.R().SetHeader("Range", "byte=-0").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+
+ resp, err = resty.R().SetHeader("Range", "byte=0").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+
+ resp, err = resty.R().SetHeader("Range", "octet=-0").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+
+ resp, err = resty.R().SetHeader("Range", "bytes=-0").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+
+ resp, err = resty.R().SetHeader("Range", "bytes=1-0").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+
+ resp, err = resty.R().SetHeader("Range", "bytes=-1-0").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
- user1 := "test"
- password1 := "test"
- testString1 := getCredString(user1, password1)
- htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
- defer os.Remove(htpasswdPath)
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- }
+ resp, err = resty.R().SetHeader("Range", "bytes=-1--0").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
- conf.HTTP.Port = port
+ resp, err = resty.R().SetHeader("Range", "bytes=1--2").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
- defaultVal := true
+ resp, err = resty.R().SetHeader("Range", "bytes=0-a").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
- searchConfig := &extconf.SearchConfig{
- BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
- }
+ resp, err = resty.R().SetHeader("Range", "bytes=a-10").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
- conf.Extensions = &extconf.ExtensionConfig{
- Search: searchConfig,
- }
+ resp, err = resty.R().SetHeader("Range", "bytes=a-b").Get(blobLoc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusRequestedRangeNotSatisfiable)
+ })
+ })
+}
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Repositories: config.Repositories{
- repoName: config.PolicyGroup{
- Policies: []config.Policy{
- {
- Users: []string{user1},
- Actions: []string{"read", "create"},
- },
- },
- DefaultPolicy: []string{},
- },
- inaccessibleRepo: config.PolicyGroup{
- Policies: []config.Policy{
- {
- Users: []string{user1},
- Actions: []string{"create"},
- },
- },
- DefaultPolicy: []string{},
- },
- },
- AdminPolicy: config.Policy{
- Users: []string{},
- Actions: []string{},
- },
- }
+func TestInjectInterruptedImageManifest(t *testing.T) {
+ Convey("Make a new controller", t, func() {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ conf := config.New()
+ conf.HTTP.Port = port
- ctlr := makeController(conf, tempDir)
+ dir := t.TempDir()
+ ctlr := makeController(conf, dir)
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- cfg, layers, manifest, err := deprecated.GetImageComponents(10000) //nolint:staticcheck
- So(err, ShouldBeNil)
+ rthdlr := api.NewRouteHandler(ctlr)
- err = UploadImageWithBasicAuth(
- Image{
- Config: cfg,
- Layers: layers,
- Manifest: manifest,
- }, baseURL, repoName, "latest",
- user1, password1)
+ Convey("Upload a blob & a config blob; Create an image manifest", func() {
+ // create a blob/layer
+ resp, err := resty.R().Post(baseURL + "/v2/repotest/blobs/uploads/")
So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
+ loc := test.Location(baseURL, resp)
+ So(loc, ShouldNotBeEmpty)
- // data for the inaccessible repo
- cfg, layers, manifest, err = deprecated.GetImageComponents(10000) //nolint:staticcheck
+ // since we are not specifying any prefix i.e provided in config while starting server,
+ // so it should store repotest to global root dir
+ _, err = os.Stat(path.Join(dir, "repotest"))
So(err, ShouldBeNil)
- err = UploadImageWithBasicAuth(
- Image{
- Config: cfg,
- Layers: layers,
- Manifest: manifest,
- }, baseURL, inaccessibleRepo, "latest",
- user1, password1)
+ resp, err = resty.R().Get(loc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
+ content := []byte("this is a dummy blob")
+ digest := godigest.FromBytes(content)
+ So(digest, ShouldNotBeNil)
+ // monolithic blob upload: success
+ resp, err = resty.R().SetQueryParam("digest", digest.String()).
+ SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc)
So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ blobLoc := resp.Header().Get("Location")
+ So(blobLoc, ShouldNotBeEmpty)
+ So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
+ So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty)
- query := `
- {
- GlobalSearch(query:"testrepo"){
- Repos {
- Name
- NewestImage {
- RepoName
- Tag
- }
- }
- }
- }`
- resp, err := resty.R().SetBasicAuth(user1, password1).Get(baseURL + constants.FullSearchPrefix +
- "?query=" + url.QueryEscape(query))
+ // upload image config blob
+ resp, err = resty.R().Post(baseURL + "/v2/repotest/blobs/uploads/")
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, 200)
- So(string(resp.Body()), ShouldContainSubstring, repoName)
- So(string(resp.Body()), ShouldNotContainSubstring, inaccessibleRepo)
+ So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
+ loc = test.Location(baseURL, resp)
+ cblob, cdigest := GetRandomImageConfig()
- resp, err = resty.R().Get(baseURL + constants.FullSearchPrefix + "?query=" + url.QueryEscape(query))
+ resp, err = resty.R().
+ SetContentLength(true).
+ 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.StatusUnauthorized)
-
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Repositories: config.Repositories{
- repoName: config.PolicyGroup{
- Policies: []config.Policy{
- {
- Users: []string{user1},
- Actions: []string{},
- },
- },
- DefaultPolicy: []string{},
- },
- inaccessibleRepo: config.PolicyGroup{
- Policies: []config.Policy{
- {
- Users: []string{},
- Actions: []string{},
- },
- },
- DefaultPolicy: []string{},
- },
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+
+ // create a manifest
+ manifest := ispec.Manifest{
+ Config: ispec.Descriptor{
+ MediaType: "application/vnd.oci.image.config.v1+json",
+ Digest: cdigest,
+ Size: int64(len(cblob)),
},
- AdminPolicy: config.Policy{
- Users: []string{},
- Actions: []string{},
+ Layers: []ispec.Descriptor{
+ {
+ MediaType: "application/vnd.oci.image.layer.v1.tar",
+ Digest: digest,
+ Size: int64(len(content)),
+ },
},
}
-
- // authenticated, but no access to resource
- resp, err = resty.R().SetBasicAuth(user1, password1).Get(baseURL + constants.FullSearchPrefix +
- "?query=" + url.QueryEscape(query))
+ manifest.SchemaVersion = 2
+ content, err = json.Marshal(manifest)
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- So(string(resp.Body()), ShouldNotContainSubstring, repoName)
- So(string(resp.Body()), ShouldNotContainSubstring, inaccessibleRepo)
+ digest = godigest.FromBytes(content)
+ So(digest, ShouldNotBeNil)
+
+ // Testing router path: @Router /v2/{name}/manifests/{reference} [put]
+ Convey("Uploading an image manifest blob (when injected simulates an interrupted image manifest upload)", func() {
+ injected := inject.InjectFailure(0)
+
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, bytes.NewReader(content))
+ request = mux.SetURLVars(request, map[string]string{"name": "repotest", "reference": "1.0"})
+ request.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
+ response := httptest.NewRecorder()
+
+ rthdlr.UpdateManifest(response, request)
+
+ resp := response.Result()
+ defer resp.Body.Close()
+
+ So(resp, ShouldNotBeNil)
+
+ if injected {
+ So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
+ } else {
+ So(resp.StatusCode, ShouldEqual, http.StatusCreated)
+ }
+ })
})
+ })
+}
- Convey("Testing group permissions", func(c C) {
- conf := config.New()
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
+func TestInjectTooManyOpenFiles(t *testing.T) {
+ Convey("Make a new controller", t, func() {
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ conf := config.New()
+ conf.HTTP.Port = port
- user1 := "test1"
- password1 := "test1"
- group1 := "testgroup3"
- testString1 := getCredString(user1, password1)
- htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
- defer os.Remove(htpasswdPath)
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- }
+ dir := t.TempDir()
+ ctlr := makeController(conf, dir)
+ conf.Storage.RemoteCache = false
- conf.HTTP.Port = port
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- defaultVal := true
+ rthdlr := api.NewRouteHandler(ctlr)
- searchConfig := &extconf.SearchConfig{
- BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
- }
+ // create a blob/layer
+ resp, err := resty.R().Post(baseURL + "/v2/repotest/blobs/uploads/")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
+ loc := test.Location(baseURL, resp)
+ So(loc, ShouldNotBeEmpty)
- conf.Extensions = &extconf.ExtensionConfig{
- Search: searchConfig,
- }
+ // since we are not specifying any prefix i.e provided in config while starting server,
+ // so it should store repotest to global root dir
+ _, err = os.Stat(path.Join(dir, "repotest"))
+ So(err, ShouldBeNil)
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Groups: config.Groups{
- group1: {
- Users: []string{user1},
- },
- },
- Repositories: config.Repositories{
- repoName: config.PolicyGroup{
- Policies: []config.Policy{
- {
- Groups: []string{group1},
- Actions: []string{"read", "create"},
- },
- },
- DefaultPolicy: []string{},
- },
- },
- AdminPolicy: config.Policy{
- Users: []string{},
- Actions: []string{},
- },
- }
+ resp, err = resty.R().Get(loc)
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
+ content := []byte("this is a dummy blob")
+ digest := godigest.FromBytes(content)
+ So(digest, ShouldNotBeNil)
- ctlr := makeController(conf, tempDir)
+ // monolithic blob upload
+ injected := inject.InjectFailure(2)
+ if injected {
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, loc, bytes.NewReader(content))
+ tokens := strings.Split(loc, "/")
+ request = mux.SetURLVars(request, map[string]string{"name": "repotest", "session_id": tokens[len(tokens)-1]})
+ q := request.URL.Query()
+ q.Add("digest", digest.String())
+ request.URL.RawQuery = q.Encode()
+ request.Header.Set("Content-Type", "application/octet-stream")
+ request.Header.Set("Content-Length", fmt.Sprintf("%d", len(content)))
+ response := httptest.NewRecorder()
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ rthdlr.UpdateBlobUpload(response, request)
- img := CreateRandomImage()
+ resp := response.Result()
+ defer resp.Body.Close()
- err = UploadImageWithBasicAuth(img, baseURL, repoName, img.DigestStr(), user1, password1)
+ So(resp, ShouldNotBeNil)
+ So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
+ } else {
+ resp, err = resty.R().SetQueryParam("digest", digest.String()).
+ SetHeader("Content-Type", "application/octet-stream").
+ SetBody(content).Put(loc)
So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- query := `
- {
- GlobalSearch(query:"testrepo"){
- Repos {
- Name
- NewestImage {
- RepoName
- Tag
- }
- }
- }
- }`
- resp, err := resty.R().SetBasicAuth(user1, password1).Get(baseURL + constants.FullSearchPrefix +
- "?query=" + url.QueryEscape(query))
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, 200)
- })
+ blobLoc := resp.Header().Get("Location")
+ So(blobLoc, ShouldNotBeEmpty)
+ So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
+ So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty)
+ }
- Convey("Testing group permissions when the user is part of more groups with different permissions", func(c C) {
- conf := config.New()
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
+ // upload image config blob
+ resp, err = resty.R().Post(baseURL + "/v2/repotest/blobs/uploads/")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
+ loc = test.Location(baseURL, resp)
+ cblob, cdigest := GetRandomImageConfig()
+
+ resp, err = resty.R().
+ SetContentLength(true).
+ 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.StatusCode(), ShouldEqual, http.StatusCreated)
- user1 := "test2"
- password1 := "test2"
- group1 := "testgroup1"
- group2 := "secondtestgroup"
- testString1 := getCredString(user1, password1)
- htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
- defer os.Remove(htpasswdPath)
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
+ // create a manifest
+ manifest := 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: digest,
+ Size: int64(len(content)),
},
- }
+ },
+ }
+ manifest.SchemaVersion = 2
+ content, err = json.Marshal(manifest)
+ So(err, ShouldBeNil)
+ digest = godigest.FromBytes(content)
+ So(digest, ShouldNotBeNil)
- conf.HTTP.Port = port
+ // Testing router path: @Router /v2/{name}/manifests/{reference} [put]
+ //nolint:lll // gofumpt conflicts with lll
+ Convey("Uploading an image manifest blob (when injected simulates that PutImageManifest failed due to 'too many open files' error)", func() {
+ injected := inject.InjectFailure(2)
- defaultVal := true
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, bytes.NewReader(content))
+ request = mux.SetURLVars(request, map[string]string{"name": "repotest", "reference": "1.0"})
+ request.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
+ response := httptest.NewRecorder()
- searchConfig := &extconf.SearchConfig{
- BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
- }
+ rthdlr.UpdateManifest(response, request)
- conf.Extensions = &extconf.ExtensionConfig{
- Search: searchConfig,
- }
+ resp := response.Result()
+ So(resp, ShouldNotBeNil)
+ defer resp.Body.Close()
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Groups: config.Groups{
- group1: {
- Users: []string{user1},
- },
- },
- Repositories: config.Repositories{
- repoName: config.PolicyGroup{
- Policies: []config.Policy{
- {
- Groups: []string{group1},
- Actions: []string{"delete"},
- },
- {
- Groups: []string{group2},
- Actions: []string{"read", "create"},
- },
- },
- DefaultPolicy: []string{},
- },
- },
- AdminPolicy: config.Policy{
- Users: []string{},
- Actions: []string{},
- },
+ if injected {
+ So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
+ } else {
+ So(resp.StatusCode, ShouldEqual, http.StatusCreated)
}
+ })
+ Convey("when injected simulates a `too many open files` error inside PutImageManifest method of img store", func() {
+ injected := inject.InjectFailure(2)
- ctlr := makeController(conf, tempDir)
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, bytes.NewReader(content))
+ request = mux.SetURLVars(request, map[string]string{"name": "repotest", "reference": "1.0"})
+ request.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
+ response := httptest.NewRecorder()
- img := CreateRandomImage()
+ rthdlr.UpdateManifest(response, request)
- err = UploadImageWithBasicAuth(img, baseURL, repoName, img.DigestStr(), user1, password1)
- So(err, ShouldNotBeNil)
- })
+ resp := response.Result()
+ defer resp.Body.Close()
- Convey("Testing group permissions when group has less permissions than user", func(c C) {
- conf := config.New()
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
+ So(resp, ShouldNotBeNil)
- user1 := "test3"
- password1 := "test3"
- group1 := "testgroup"
- testString1 := getCredString(user1, password1)
- htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
- defer os.Remove(htpasswdPath)
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
+ if injected {
+ So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
+ } else {
+ So(resp.StatusCode, ShouldEqual, http.StatusCreated)
}
+ })
+ Convey("code coverage: error inside PutImageManifest method of img store (unable to marshal JSON)", func() {
+ injected := inject.InjectFailure(1)
- conf.HTTP.Port = port
+ request, _ := http.NewRequestWithContext(context.TODO(), http.MethodPut, baseURL, bytes.NewReader(content))
+ request = mux.SetURLVars(request, map[string]string{"name": "repotest", "reference": "1.0"})
+ request.Header.Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
+ response := httptest.NewRecorder()
- defaultVal := true
+ rthdlr.UpdateManifest(response, request)
- searchConfig := &extconf.SearchConfig{
- BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
- }
+ resp := response.Result()
+ defer resp.Body.Close()
- conf.Extensions = &extconf.ExtensionConfig{
- Search: searchConfig,
- }
+ So(resp, ShouldNotBeNil)
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Groups: config.Groups{
- group1: {
- Users: []string{user1},
- },
- },
- Repositories: config.Repositories{
- repoName: config.PolicyGroup{
- Policies: []config.Policy{
- {
- Groups: []string{group1},
- Actions: []string{"delete"},
- },
- {
- Users: []string{user1},
- Actions: []string{"read", "create", "delete"},
- },
- },
- DefaultPolicy: []string{},
- },
- },
- AdminPolicy: config.Policy{
- Users: []string{},
- Actions: []string{},
- },
+ if injected {
+ So(resp.StatusCode, ShouldEqual, http.StatusInternalServerError)
+ } else {
+ So(resp.StatusCode, ShouldEqual, http.StatusCreated)
}
+ })
- ctlr := makeController(conf, tempDir)
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ Convey("when index.json is not in json format", func() {
+ resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
+ SetBody(content).Put(baseURL + "/v2/repotest/manifests/v1.0")
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
+ digestHdr := resp.Header().Get(constants.DistContentDigestKey)
+ So(digestHdr, ShouldNotBeEmpty)
+ So(digestHdr, ShouldEqual, digest.String())
- img := CreateRandomImage()
+ indexFile := path.Join(dir, "repotest", "index.json")
+ _, err = os.Stat(indexFile)
+ So(err, ShouldBeNil)
+ indexContent := []byte(`not a JSON content`)
+ err = os.WriteFile(indexFile, indexContent, 0o600)
+ So(err, ShouldBeNil)
- err = UploadImageWithBasicAuth(img, baseURL, repoName, img.DigestStr(), user1, password1)
+ resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json").
+ SetBody(content).Put(baseURL + "/v2/repotest/manifests/v1.1")
So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusInternalServerError)
})
+ })
+}
+
+func TestGCSignaturesAndUntaggedManifestsWithMetaDB(t *testing.T) {
+ Convey("Make controller", t, func() {
+ Convey("Garbage collect signatures without subject and manifests without tags", func(c C) {
+ repoName := "testrepo" //nolint:goconst
+ tag := "0.0.1"
- Convey("Testing group permissions when user has less permissions than group", func(c C) {
- conf := config.New()
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
-
- user1 := "test4"
- password1 := "test4"
- group1 := "testgroup1"
- testString1 := getCredString(user1, password1)
- htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
- defer os.Remove(htpasswdPath)
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- }
-
+ conf := config.New()
conf.HTTP.Port = port
- defaultVal := true
-
+ value := true
searchConfig := &extconf.SearchConfig{
- BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
+ BaseConfig: extconf.BaseConfig{Enable: &value},
}
+ // added search extensions so that metaDB is initialized and its tested in GC logic
conf.Extensions = &extconf.ExtensionConfig{
Search: searchConfig,
}
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Groups: config.Groups{
- group1: {
- Users: []string{user1},
- },
- },
- Repositories: config.Repositories{
- repoName: config.PolicyGroup{
- Policies: []config.Policy{
- {
- Groups: []string{group1},
- Actions: []string{"read", "create", "delete"},
- },
- {
- Users: []string{user1},
- Actions: []string{"delete"},
- },
- },
- DefaultPolicy: []string{},
- },
- },
- AdminPolicy: config.Policy{
- Users: []string{},
- Actions: []string{},
- },
- }
+ ctlr := makeController(conf, t.TempDir())
+
+ dir := t.TempDir()
+ ctlr.Config.Storage.RootDirectory = dir
+ ctlr.Config.Storage.GC = true
+ ctlr.Config.Storage.GCDelay = 1 * time.Millisecond
+ ctlr.Config.Storage.UntaggedImageRetentionDelay = 1 * time.Millisecond
- ctlr := makeController(conf, tempDir)
+ ctlr.Config.Storage.Dedupe = false
cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
+ cm.StartServer()
+ cm.WaitServerToBeReady(port)
defer cm.StopServer()
- img := CreateRandomImage()
+ img := CreateDefaultImage()
- err = UploadImageWithBasicAuth(img, baseURL, repoName, img.DigestStr(), user1, password1)
+ err := UploadImage(img, baseURL, repoName, tag)
So(err, ShouldBeNil)
- })
-
- Convey("Testing group permissions on admin policy", func(c C) {
- conf := config.New()
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
-
- user1 := "test5"
- password1 := "test5"
- group1 := "testgroup2"
- testString1 := getCredString(user1, password1)
- htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
- defer os.Remove(htpasswdPath)
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- }
-
- conf.HTTP.Port = port
-
- defaultVal := true
-
- searchConfig := &extconf.SearchConfig{
- BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
- }
-
- conf.Extensions = &extconf.ExtensionConfig{
- Search: searchConfig,
- }
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Groups: config.Groups{
- group1: {
- Users: []string{user1},
- },
- },
- Repositories: config.Repositories{},
- AdminPolicy: config.Policy{
- Groups: []string{group1},
- Actions: []string{"read", "create"},
+ gc := gc.NewGarbageCollect(ctlr.StoreController.DefaultStore, ctlr.MetaDB,
+ gc.Options{
+ Referrers: ctlr.Config.Storage.GCReferrers,
+ Delay: ctlr.Config.Storage.GCDelay,
+ RetentionDelay: ctlr.Config.Storage.UntaggedImageRetentionDelay,
},
- }
-
- ctlr := makeController(conf, tempDir)
-
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
-
- img := CreateRandomImage()
+ ctlr.Log)
- err = UploadImageWithBasicAuth(img, baseURL, repoName, img.DigestStr(), user1, password1)
+ resp, err := resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, tag))
So(err, ShouldBeNil)
- })
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ digest := godigest.FromBytes(resp.Body())
+ So(digest, ShouldNotBeEmpty)
- Convey("Testing group permissions on anonymous policy", func(c C) {
- conf := config.New()
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
+ cwd, err := os.Getwd()
+ So(err, ShouldBeNil)
+ defer func() { _ = os.Chdir(cwd) }()
+ tdir := t.TempDir()
+ _ = os.Chdir(tdir)
- conf.HTTP.Port = port
+ // generate a keypair
+ os.Setenv("COSIGN_PASSWORD", "")
+ err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil)
+ So(err, ShouldBeNil)
- defaultVal := true
- group1 := group
- user1 := username
- password1 := passphrase
-
- testString1 := getCredString(user1, password1)
- htpasswdPath := test.MakeHtpasswdFileFromString(testString1)
- defer os.Remove(htpasswdPath)
- conf.HTTP.Auth = &config.AuthConfig{
- HTPasswd: config.AuthHTPasswd{
- Path: htpasswdPath,
- },
- }
+ image := fmt.Sprintf("localhost:%s/%s@%s", port, repoName, digest.String())
- searchConfig := &extconf.SearchConfig{
- BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
- }
+ annotations := []string{fmt.Sprintf("tag=%s", tag)}
- conf.Extensions = &extconf.ExtensionConfig{
- Search: searchConfig,
- }
- conf.HTTP.AccessControl = &config.AccessControlConfig{
- Groups: config.Groups{
- group1: {
- Users: []string{user1},
- },
- },
- Repositories: config.Repositories{
- repoName: config.PolicyGroup{
- Policies: []config.Policy{
- {
- Groups: []string{group1},
- Actions: []string{"read", "create", "delete"},
- },
- {
- Users: []string{user1},
- Actions: []string{"delete"},
- },
- },
- DefaultPolicy: []string{},
- AnonymousPolicy: []string{"read", "create"},
- },
+ // sign the image
+ err = sign.SignCmd(&options.RootOptions{Verbose: true, Timeout: 1 * time.Minute},
+ options.KeyOpts{KeyRef: path.Join(tdir, "cosign.key"), PassFunc: generate.GetPass},
+ options.SignOptions{
+ Registry: options.RegistryOptions{AllowInsecure: true},
+ AnnotationOptions: options.AnnotationOptions{Annotations: annotations},
+ Upload: true,
},
- }
+ []string{image})
- ctlr := makeController(conf, tempDir)
+ So(err, ShouldBeNil)
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ signature.NotationPathLock.Lock()
+ defer signature.NotationPathLock.Unlock()
- img := CreateRandomImage()
+ signature.LoadNotationPath(tdir)
- err = UploadImageWithBasicAuth(img, baseURL, repoName, img.DigestStr(), "", "")
+ // generate a keypair
+ err = signature.GenerateNotationCerts(tdir, "good")
So(err, ShouldBeNil)
- })
- })
-}
-
-func TestDistSpecExtensions(t *testing.T) {
- Convey("start zot server with search, ui and trust extensions", t, func(c C) {
- conf := config.New()
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
- conf.HTTP.Port = port
+ // sign the image
+ err = signature.SignWithNotation("good", image, tdir)
+ So(err, ShouldBeNil)
- defaultVal := true
+ // get cosign signature manifest
+ cosignTag := strings.Replace(digest.String(), ":", "-", 1) + "." + remote.SignatureTagSuffix
- conf.Extensions = &extconf.ExtensionConfig{}
- conf.Extensions.Search = &extconf.SearchConfig{}
- conf.Extensions.Search.Enable = &defaultVal
- conf.Extensions.Search.CVE = nil
- conf.Extensions.UI = &extconf.UIConfig{}
- conf.Extensions.UI.Enable = &defaultVal
- conf.Extensions.Trust = &extconf.ImageTrustConfig{}
- conf.Extensions.Trust.Enable = &defaultVal
- conf.Extensions.Trust.Cosign = defaultVal
- conf.Extensions.Trust.Notation = defaultVal
+ resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, cosignTag))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- logFile, err := os.CreateTemp("", "zot-log*.txt")
- So(err, ShouldBeNil)
- conf.Log.Output = logFile.Name()
- defer os.Remove(logFile.Name()) // clean up
+ cosignDigest := resp.Header().Get(constants.DistContentDigestKey)
+ So(cosignDigest, ShouldNotBeEmpty)
- ctlr := makeController(conf, t.TempDir())
+ // get notation signature manifest
+ resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
+ fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String()))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ var index ispec.Index
- var extensionList distext.ExtensionList
+ err = json.Unmarshal(resp.Body(), &index)
+ So(err, ShouldBeNil)
+ So(len(index.Manifests), ShouldEqual, 1)
- resp, err := resty.R().Get(baseURL + constants.RoutePrefix + constants.ExtOciDiscoverPrefix)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, 200)
- err = json.Unmarshal(resp.Body(), &extensionList)
- So(err, ShouldBeNil)
- t.Log(extensionList.Extensions)
- So(len(extensionList.Extensions), ShouldEqual, 1)
- So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 5)
- So(extensionList.Extensions[0].Name, ShouldEqual, constants.BaseExtension)
- So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md")
- So(extensionList.Extensions[0].Description, ShouldNotBeEmpty)
- // Verify the endpoints below are enabled by search
- So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullSearchPrefix)
- // Verify the endpoints below are enabled by trust
- So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullCosign)
- So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullNotation)
- // Verify the endpint below are enabled by having both the UI and the Search enabled
- So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullMgmt)
- So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullUserPrefs)
- })
+ // shouldn't do anything
+ err = gc.CleanRepo(repoName)
+ So(err, ShouldBeNil)
- Convey("start zot server with only the search extension enabled", t, func(c C) {
- conf := config.New()
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
+ // make sure both signatures are stored in repodb
+ repoMeta, err := ctlr.MetaDB.GetRepoMeta(repoName)
+ So(err, ShouldBeNil)
- conf.HTTP.Port = port
+ sigMeta := repoMeta.Signatures[digest.String()]
+ So(len(sigMeta[storage.CosignType]), ShouldEqual, 1)
+ So(len(sigMeta[storage.NotationType]), ShouldEqual, 1)
+ So(sigMeta[storage.CosignType][0].SignatureManifestDigest, ShouldEqual, cosignDigest)
+ So(sigMeta[storage.NotationType][0].SignatureManifestDigest, ShouldEqual, index.Manifests[0].Digest.String())
- defaultVal := true
+ Convey("Trigger gcNotationSignatures() error", func() {
+ var refs ispec.Index
+ err = json.Unmarshal(resp.Body(), &refs)
- conf.Extensions = &extconf.ExtensionConfig{}
- conf.Extensions.Search = &extconf.SearchConfig{}
- conf.Extensions.Search.Enable = &defaultVal
+ err := os.Chmod(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded()), 0o000)
+ So(err, ShouldBeNil)
- logFile, err := os.CreateTemp("", "zot-log*.txt")
- So(err, ShouldBeNil)
- conf.Log.Output = logFile.Name()
- defer os.Remove(logFile.Name()) // clean up
+ // trigger gc
+ img := CreateRandomImage()
- ctlr := makeController(conf, t.TempDir())
+ err = UploadImage(img, baseURL, repoName, img.DigestStr())
+ So(err, ShouldBeNil)
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ err = gc.CleanRepo(repoName)
+ So(err, ShouldNotBeNil)
- var extensionList distext.ExtensionList
+ err = os.Chmod(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded()), 0o755)
+ So(err, ShouldBeNil)
- resp, err := resty.R().Get(baseURL + constants.RoutePrefix + constants.ExtOciDiscoverPrefix)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, 200)
- err = json.Unmarshal(resp.Body(), &extensionList)
- So(err, ShouldBeNil)
- t.Log(extensionList.Extensions)
- So(len(extensionList.Extensions), ShouldEqual, 1)
- So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 2)
- So(extensionList.Extensions[0].Name, ShouldEqual, constants.BaseExtension)
- So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md")
- So(extensionList.Extensions[0].Description, ShouldNotBeEmpty)
- // Verify the endpoints below are enabled by search
- So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullSearchPrefix)
- So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullMgmt)
- // Verify the endpoints below are not enabled since trust is not enabled
- So(extensionList.Extensions[0].Endpoints, ShouldNotContain, constants.FullCosign)
- So(extensionList.Extensions[0].Endpoints, ShouldNotContain, constants.FullNotation)
- // Verify the endpoints below are not enabled since the UI is not enabled
- So(extensionList.Extensions[0].Endpoints, ShouldNotContain, constants.FullUserPrefs)
- })
+ content, err := os.ReadFile(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded()))
+ So(err, ShouldBeNil)
+ err = os.WriteFile(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded()), []byte("corrupt"), 0o600) //nolint:lll
+ So(err, ShouldBeNil)
- Convey("start zot server with no enabled extensions", t, func(c C) {
- conf := config.New()
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
+ err = UploadImage(img, baseURL, repoName, tag)
+ So(err, ShouldBeNil)
- conf.HTTP.Port = port
+ err = gc.CleanRepo(repoName)
+ So(err, ShouldNotBeNil)
- logFile, err := os.CreateTemp("", "zot-log*.txt")
- So(err, ShouldBeNil)
- conf.Log.Output = logFile.Name()
- defer os.Remove(logFile.Name()) // clean up
+ err = os.WriteFile(path.Join(dir, repoName, "blobs", "sha256", refs.Manifests[0].Digest.Encoded()), content, 0o600)
+ So(err, ShouldBeNil)
+ })
- ctlr := makeController(conf, t.TempDir())
+ Convey("Overwrite original image, signatures should be garbage-collected", func() {
+ // push an image without tag
+ cfg, layers, manifest, err := deprecated.GetImageComponents(2) //nolint:staticcheck
+ So(err, ShouldBeNil)
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ manifestBuf, err := json.Marshal(manifest)
+ So(err, ShouldBeNil)
+ untaggedManifestDigest := godigest.FromBytes(manifestBuf)
- var extensionList distext.ExtensionList
+ err = UploadImage(
+ Image{
+ Config: cfg,
+ Layers: layers,
+ Manifest: manifest,
+ }, baseURL, repoName, untaggedManifestDigest.String())
+ So(err, ShouldBeNil)
- resp, err := resty.R().Get(baseURL + constants.RoutePrefix + constants.ExtOciDiscoverPrefix)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, 200)
- err = json.Unmarshal(resp.Body(), &extensionList)
- So(err, ShouldBeNil)
- t.Log(extensionList.Extensions)
- // Verify all endpoints which are disabled (even signing urls depend on search being enabled)
- So(len(extensionList.Extensions), ShouldEqual, 0)
- })
+ // make sure repoDB reference was added
+ repoMeta, err := ctlr.MetaDB.GetRepoMeta(repoName)
+ So(err, ShouldBeNil)
- Convey("start minimal zot server", t, func(c C) {
- conf := config.New()
- port := test.GetFreePort()
- baseURL := test.GetBaseURL(port)
+ _, ok := repoMeta.Referrers[untaggedManifestDigest.String()]
+ So(ok, ShouldBeTrue)
+ _, ok = repoMeta.Signatures[untaggedManifestDigest.String()]
+ So(ok, ShouldBeTrue)
+ _, ok = repoMeta.Statistics[untaggedManifestDigest.String()]
+ So(ok, ShouldBeTrue)
- conf.HTTP.Port = port
+ // overwrite image so that signatures will get invalidated and gc'ed
+ cfg, layers, manifest, err = deprecated.GetImageComponents(3) //nolint:staticcheck
+ So(err, ShouldBeNil)
- logFile, err := os.CreateTemp("", "zot-log*.txt")
- So(err, ShouldBeNil)
- conf.Log.Output = logFile.Name()
- defer os.Remove(logFile.Name()) // clean up
+ err = UploadImage(
+ Image{
+ Config: cfg,
+ Layers: layers,
+ Manifest: manifest,
+ }, baseURL, repoName, tag)
+ So(err, ShouldBeNil)
- ctlr := makeController(conf, t.TempDir())
+ manifestBuf, err = json.Marshal(manifest)
+ So(err, ShouldBeNil)
+ newManifestDigest := godigest.FromBytes(manifestBuf)
- cm := test.NewControllerManager(ctlr)
- cm.StartAndWait(port)
- defer cm.StopServer()
+ err = gc.CleanRepo(repoName)
+ So(err, ShouldBeNil)
- var extensionList distext.ExtensionList
- resp, err := resty.R().Get(baseURL + constants.RoutePrefix + constants.ExtOciDiscoverPrefix)
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, 200)
+ // make sure both signatures are removed from repodb and repo reference for untagged is removed
+ repoMeta, err = ctlr.MetaDB.GetRepoMeta(repoName)
+ So(err, ShouldBeNil)
- err = json.Unmarshal(resp.Body(), &extensionList)
- So(err, ShouldBeNil)
- So(len(extensionList.Extensions), ShouldEqual, 0)
- })
-}
+ sigMeta := repoMeta.Signatures[digest.String()]
+ So(len(sigMeta[storage.CosignType]), ShouldEqual, 0)
+ So(len(sigMeta[storage.NotationType]), ShouldEqual, 0)
-func TestHTTPOptionsResponse(t *testing.T) {
- Convey("Test http options response", t, func() {
- conf := config.New()
- port := test.GetFreePort()
- conf.HTTP.Port = port
- baseURL := test.GetBaseURL(port)
+ _, ok = repoMeta.Referrers[untaggedManifestDigest.String()]
+ So(ok, ShouldBeFalse)
+ _, ok = repoMeta.Signatures[untaggedManifestDigest.String()]
+ So(ok, ShouldBeFalse)
+ _, ok = repoMeta.Statistics[untaggedManifestDigest.String()]
+ So(ok, ShouldBeFalse)
- ctlr := api.NewController(conf)
+ // both signatures should be gc'ed
+ resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, cosignTag))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
- firstDir := t.TempDir()
+ resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
+ fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, digest.String()))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- secondDir := t.TempDir()
- defer os.RemoveAll(firstDir)
- defer os.RemoveAll(secondDir)
+ err = json.Unmarshal(resp.Body(), &index)
+ So(err, ShouldBeNil)
+ So(len(index.Manifests), ShouldEqual, 0)
- ctlr.Config.Storage.RootDirectory = firstDir
- subPaths := make(map[string]config.StorageConfig)
- subPaths["/a"] = config.StorageConfig{
- RootDirectory: secondDir,
- }
+ resp, err = resty.R().SetQueryParam("artifactType", notreg.ArtifactTypeNotation).Get(
+ fmt.Sprintf("%s/v2/%s/referrers/%s", baseURL, repoName, newManifestDigest.String()))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
- ctlr.Config.Storage.SubPaths = subPaths
- ctrlManager := test.NewControllerManager(ctlr)
+ err = json.Unmarshal(resp.Body(), &index)
+ So(err, ShouldBeNil)
+ So(len(index.Manifests), ShouldEqual, 0)
- ctrlManager.StartAndWait(port)
+ // untagged image should also be gc'ed
+ resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, untaggedManifestDigest))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)
+ })
+ })
- resp, _ := resty.R().Options(baseURL + constants.RoutePrefix + constants.ExtCatalogPrefix)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
+ Convey("Do not gc manifests which are part of a multiarch image", func(c C) {
+ repoName := "testrepo" //nolint:goconst
+ tag := "0.0.1"
- defer ctrlManager.StopServer()
- })
-}
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
+ conf := config.New()
+ conf.HTTP.Port = port
-func TestGetGithubUserInfo(t *testing.T) {
- Convey("github api calls works", t, func() {
- mockedHTTPClient := mock.NewMockedHTTPClient(
- mock.WithRequestMatch(
- mock.GetUserEmails,
- []github.UserEmail{
- {
- Email: github.String("test@test"),
- Primary: github.Bool(true),
- },
- },
- ),
- mock.WithRequestMatch(
- mock.GetUserOrgs,
- []github.Organization{
- {
- Login: github.String("testOrg"),
- },
- },
- ),
- )
+ ctlr := makeController(conf, t.TempDir())
- client := github.NewClient(mockedHTTPClient)
+ dir := t.TempDir()
+ ctlr.Config.Storage.RootDirectory = dir
+ ctlr.Config.Storage.GC = true
+ ctlr.Config.Storage.GCDelay = 1 * time.Second
+ ctlr.Config.Storage.UntaggedImageRetentionDelay = 1 * time.Second
- _, _, err := api.GetGithubUserInfo(context.Background(), client, log.Logger{})
- So(err, ShouldBeNil)
- })
+ err := WriteImageToFileSystem(CreateDefaultImage(), repoName, tag,
+ ociutils.GetDefaultStoreController(dir, ctlr.Log))
+ So(err, ShouldBeNil)
- Convey("github ListEmails error", t, func() {
- mockedHTTPClient := mock.NewMockedHTTPClient(
- mock.WithRequestMatchHandler(
- mock.GetUserEmails,
- http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- mock.WriteError(
- w,
- http.StatusInternalServerError,
- "github error",
- )
- }),
- ),
- )
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- client := github.NewClient(mockedHTTPClient)
+ gc := gc.NewGarbageCollect(ctlr.StoreController.DefaultStore, ctlr.MetaDB,
+ gc.Options{
+ Referrers: ctlr.Config.Storage.GCReferrers,
+ Delay: ctlr.Config.Storage.GCDelay,
+ RetentionDelay: ctlr.Config.Storage.UntaggedImageRetentionDelay,
+ }, ctlr.Log)
- _, _, err := api.GetGithubUserInfo(context.Background(), client, log.Logger{})
- So(err, ShouldNotBeNil)
- })
+ resp, err := resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, tag))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ digest := godigest.FromBytes(resp.Body())
+ So(digest, ShouldNotBeEmpty)
- Convey("github ListEmails error", t, func() {
- mockedHTTPClient := mock.NewMockedHTTPClient(
- mock.WithRequestMatch(
- mock.GetUserEmails,
- []github.UserEmail{
- {
- Email: github.String("test@test"),
- Primary: github.Bool(true),
- },
- },
- ),
- mock.WithRequestMatchHandler(
- mock.GetUserOrgs,
- http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- mock.WriteError(
- w,
- http.StatusInternalServerError,
- "github error",
- )
- }),
- ),
- )
+ // push an image index and make sure manifests contained by it are not gc'ed
+ // create an image index on upstream
+ var index ispec.Index
+ index.SchemaVersion = 2
+ index.MediaType = ispec.MediaTypeImageIndex
- client := github.NewClient(mockedHTTPClient)
+ // upload multiple manifests
+ for i := 0; i < 4; i++ {
+ config, layers, manifest, err := deprecated.GetImageComponents(1000 + i) //nolint:staticcheck
+ So(err, ShouldBeNil)
- _, _, err := api.GetGithubUserInfo(context.Background(), client, log.Logger{})
- So(err, ShouldNotBeNil)
- })
-}
+ manifestContent, err := json.Marshal(manifest)
+ So(err, ShouldBeNil)
-func getAllBlobs(imagePath string) []string {
- blobList := make([]string, 0)
+ manifestDigest := godigest.FromBytes(manifestContent)
- if !common.DirExists(imagePath) {
- return []string{}
- }
+ err = UploadImage(
+ Image{
+ Manifest: manifest,
+ Config: config,
+ Layers: layers,
+ }, baseURL, repoName, manifestDigest.String())
+ So(err, ShouldBeNil)
- buf, err := os.ReadFile(path.Join(imagePath, "index.json"))
- if err != nil {
- panic(err)
- }
+ index.Manifests = append(index.Manifests, ispec.Descriptor{
+ Digest: manifestDigest,
+ MediaType: ispec.MediaTypeImageManifest,
+ Size: int64(len(manifestContent)),
+ })
+ }
- var index ispec.Index
- if err := json.Unmarshal(buf, &index); err != nil {
- panic(err)
- }
+ content, err := json.Marshal(index)
+ So(err, ShouldBeNil)
+ indexDigest := godigest.FromBytes(content)
+ So(indexDigest, ShouldNotBeNil)
- var digest godigest.Digest
+ time.Sleep(1 * time.Second)
+ // upload image index
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ SetBody(content).Put(baseURL + fmt.Sprintf("/v2/%s/manifests/latest", repoName))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
- for _, m := range index.Manifests {
- digest = m.Digest
- blobList = append(blobList, digest.Encoded())
- p := path.Join(imagePath, "blobs", digest.Algorithm().String(), digest.Encoded())
+ err = gc.CleanRepo(repoName)
+ So(err, ShouldBeNil)
- buf, err = os.ReadFile(p)
+ resp, err = resty.R().SetHeader("Content-Type", ispec.MediaTypeImageIndex).
+ Get(baseURL + fmt.Sprintf("/v2/%s/manifests/latest", repoName))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ So(resp.Body(), ShouldNotBeEmpty)
+ So(resp.Header().Get("Content-Type"), ShouldNotBeEmpty)
- if err != nil {
- panic(err)
- }
+ // make sure manifests which are part of image index are not gc'ed
+ for _, manifest := range index.Manifests {
+ resp, err = resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, manifest.Digest.String()))
+ So(err, ShouldBeNil)
+ So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ }
+ })
+ })
+}
- var manifest ispec.Manifest
- if err := json.Unmarshal(buf, &manifest); err != nil {
- panic(err)
- }
+func TestPeriodicGC(t *testing.T) {
+ Convey("Periodic gc enabled for default store", t, func() {
+ repoName := "testrepo" //nolint:goconst
- blobList = append(blobList, manifest.Config.Digest.Encoded())
+ port := test.GetFreePort()
+ conf := config.New()
+ conf.HTTP.Port = port
+ conf.Storage.RemoteCache = false
- for _, layer := range manifest.Layers {
- blobList = append(blobList, layer.Digest.Encoded())
- }
- }
+ logFile, err := os.CreateTemp("", "zot-log*.txt")
+ So(err, ShouldBeNil)
+ conf.Log.Output = logFile.Name()
+ defer os.Remove(logFile.Name()) // clean up
- return blobList
-}
+ ctlr := api.NewController(conf)
+ dir := t.TempDir()
+ ctlr.Config.Storage.RootDirectory = dir
+ ctlr.Config.Storage.Dedupe = false
+ ctlr.Config.Storage.GC = true
+ ctlr.Config.Storage.GCInterval = 1 * time.Hour
+ ctlr.Config.Storage.GCDelay = 1 * time.Second
-func getAllManifests(imagePath string) []string {
- manifestList := make([]string, 0)
+ err = WriteImageToFileSystem(CreateDefaultImage(), repoName, "0.0.1",
+ ociutils.GetDefaultStoreController(dir, ctlr.Log))
+ So(err, ShouldBeNil)
- if !common.DirExists(imagePath) {
- return []string{}
- }
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- buf, err := os.ReadFile(path.Join(imagePath, "index.json"))
- if err != nil {
- panic(err)
- }
+ time.Sleep(5000 * time.Millisecond)
- var index ispec.Index
- if err := json.Unmarshal(buf, &index); err != nil {
- panic(err)
- }
+ data, err := os.ReadFile(logFile.Name())
+ So(err, ShouldBeNil)
+ So(string(data), ShouldContainSubstring,
+ "\"GC\":true,\"Commit\":false,\"GCDelay\":1000000000,\"GCInterval\":3600000000000")
+ So(string(data), ShouldContainSubstring,
+ fmt.Sprintf("executing GC of orphaned blobs for %s", path.Join(ctlr.StoreController.DefaultStore.RootDir(), repoName))) //nolint:lll
+ So(string(data), ShouldContainSubstring,
+ fmt.Sprintf("GC successfully completed for %s", path.Join(ctlr.StoreController.DefaultStore.RootDir(), repoName))) //nolint:lll
+ })
- var digest godigest.Digest
+ Convey("Periodic GC enabled for substore", t, func() {
+ port := test.GetFreePort()
+ conf := config.New()
+ conf.HTTP.Port = port
- for _, m := range index.Manifests {
- digest = m.Digest
- manifestList = append(manifestList, digest.Encoded())
- }
+ logFile, err := os.CreateTemp("", "zot-log*.txt")
+ So(err, ShouldBeNil)
+ conf.Log.Output = logFile.Name()
+ defer os.Remove(logFile.Name()) // clean up
- return manifestList
-}
+ dir := t.TempDir()
+ ctlr := makeController(conf, dir)
+ subDir := t.TempDir()
-func makeController(conf *config.Config, dir string) *api.Controller {
- ctlr := api.NewController(conf)
+ subPaths := make(map[string]config.StorageConfig)
- ctlr.Config.Storage.RootDirectory = dir
+ subPaths["/a"] = config.StorageConfig{
+ RootDirectory: subDir, GC: true, GCDelay: 1 * time.Second,
+ UntaggedImageRetentionDelay: 1 * time.Second, GCInterval: 24 * time.Hour, RemoteCache: false, Dedupe: false,
+ } //nolint:lll // gofumpt conflicts with lll
+ ctlr.Config.Storage.Dedupe = false
+ ctlr.Config.Storage.SubPaths = subPaths
- return ctlr
-}
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
-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)
-}
+ data, err := os.ReadFile(logFile.Name())
+ So(err, ShouldBeNil)
+ // periodic GC is enabled by default for default store with a default interval
+ So(string(data), ShouldContainSubstring,
+ "\"GCDelay\":3600000000000,\"GCInterval\":3600000000000,\"")
+ // periodic GC is enabled for sub store
+ So(string(data), ShouldContainSubstring,
+ fmt.Sprintf("\"SubPaths\":{\"/a\":{\"RootDirectory\":\"%s\",\"Dedupe\":false,\"RemoteCache\":false,\"GC\":true,\"Commit\":false,\"GCDelay\":1000000000,\"GCInterval\":86400000000000", subDir)) //nolint:lll // gofumpt conflicts with lll
+ })
-func RunAuthorizationTests(t *testing.T, client *resty.Client, baseURL string, conf *config.Config) {
- t.Helper()
+ Convey("Periodic gc error", t, func() {
+ repoName := "testrepo" //nolint:goconst
- Convey("run authorization tests", func() {
- blob := []byte("hello, blob!")
- digest := godigest.FromBytes(blob).String()
+ port := test.GetFreePort()
+ conf := config.New()
+ conf.HTTP.Port = port
+ conf.Storage.RemoteCache = false
- // unauthenticated clients should not have access to /v2/
- resp, err := resty.R().Get(baseURL + "/v2/")
+ logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, 401)
+ conf.Log.Output = logFile.Name()
+ defer os.Remove(logFile.Name()) // clean up
- // 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)
+ ctlr := api.NewController(conf)
+ dir := t.TempDir()
+ ctlr.Config.Storage.RootDirectory = dir
+ ctlr.Config.Storage.Dedupe = false
- // 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)
+ ctlr.Config.Storage.GC = true
+ ctlr.Config.Storage.GCInterval = 1 * time.Hour
+ ctlr.Config.Storage.GCDelay = 1 * time.Second
- // should get 403 without create
- resp, err = client.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/")
+ err = WriteImageToFileSystem(CreateDefaultImage(), repoName, "0.0.1",
+ ociutils.GetDefaultStoreController(dir, ctlr.Log))
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
+ So(os.Chmod(dir, 0o000), ShouldBeNil)
- conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions = append(conf.HTTP.AccessControl.Repositories[AuthorizationAllRepos].Policies[0].Actions, "create") //nolint:lll // gofumpt conflicts with lll
+ defer func() {
+ So(os.Chmod(dir, 0o755), ShouldBeNil)
+ }()
- // 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")
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- // 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)
+ time.Sleep(5000 * time.Millisecond)
- // head blob should get 403 without read perm
- resp, err = client.R().Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
+ data, err := os.ReadFile(logFile.Name())
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
+ So(string(data), ShouldContainSubstring,
+ "\"GC\":true,\"Commit\":false,\"GCDelay\":1000000000,\"GCInterval\":3600000000000")
+ So(string(data), ShouldContainSubstring, "failure walking storage root-dir") //nolint:lll
+ })
+}
- // 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)
+func TestDistSpecExtensions(t *testing.T) {
+ Convey("start zot server with search, ui and trust extensions", t, func(c C) {
+ conf := config.New()
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
- // 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
+ conf.HTTP.Port = port
- resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ defaultVal := true
- // 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)
+ conf.Extensions = &extconf.ExtensionConfig{}
+ conf.Extensions.Search = &extconf.SearchConfig{}
+ conf.Extensions.Search.Enable = &defaultVal
+ conf.Extensions.Search.CVE = nil
+ conf.Extensions.UI = &extconf.UIConfig{}
+ conf.Extensions.UI.Enable = &defaultVal
+ conf.Extensions.Trust = &extconf.ImageTrustConfig{}
+ conf.Extensions.Trust.Enable = &defaultVal
+ conf.Extensions.Trust.Cosign = defaultVal
+ conf.Extensions.Trust.Notation = defaultVal
- // get blob should get 200 now
- resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
+ logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ conf.Log.Output = logFile.Name()
+ defer os.Remove(logFile.Name()) // clean up
- // 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)
+ ctlr := makeController(conf, t.TempDir())
- // 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
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
+
+ var extensionList distext.ExtensionList
- // delete blob should get 202
- resp, err = client.R().Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
+ resp, err := resty.R().Get(baseURL + constants.RoutePrefix + constants.ExtOciDiscoverPrefix)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
+ So(resp.StatusCode(), ShouldEqual, 200)
+ err = json.Unmarshal(resp.Body(), &extensionList)
+ So(err, ShouldBeNil)
+ t.Log(extensionList.Extensions)
+ So(len(extensionList.Extensions), ShouldEqual, 1)
+ So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 5)
+ So(extensionList.Extensions[0].Name, ShouldEqual, constants.BaseExtension)
+ So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md")
+ So(extensionList.Extensions[0].Description, ShouldNotBeEmpty)
+ // Verify the endpoints below are enabled by search
+ So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullSearchPrefix)
+ // Verify the endpoints below are enabled by trust
+ So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullCosign)
+ So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullNotation)
+ // Verify the endpint below are enabled by having both the UI and the Search enabled
+ So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullMgmt)
+ So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullUserPrefs)
+ })
- // 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{},
- }
+ Convey("start zot server with only the search extension enabled", t, func(c C) {
+ conf := config.New()
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
- 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
+ conf.HTTP.Port = port
- // 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")
+ defaultVal := true
- // 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)
+ conf.Extensions = &extconf.ExtensionConfig{}
+ conf.Extensions.Search = &extconf.SearchConfig{}
+ conf.Extensions.Search.Enable = &defaultVal
- // head blob should get 403 without read perm
- resp, err = client.R().Head(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
+ logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
+ conf.Log.Output = logFile.Name()
+ defer os.Remove(logFile.Name()) // clean up
- // 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)
+ ctlr := makeController(conf, t.TempDir())
- // 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
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ var extensionList distext.ExtensionList
- resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
+ resp, err := resty.R().Get(baseURL + constants.RoutePrefix + constants.ExtOciDiscoverPrefix)
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(resp.StatusCode(), ShouldEqual, 200)
+ err = json.Unmarshal(resp.Body(), &extensionList)
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ t.Log(extensionList.Extensions)
+ So(len(extensionList.Extensions), ShouldEqual, 1)
+ So(len(extensionList.Extensions[0].Endpoints), ShouldEqual, 2)
+ So(extensionList.Extensions[0].Name, ShouldEqual, constants.BaseExtension)
+ So(extensionList.Extensions[0].URL, ShouldContainSubstring, "_zot.md")
+ So(extensionList.Extensions[0].Description, ShouldNotBeEmpty)
+ // Verify the endpoints below are enabled by search
+ So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullSearchPrefix)
+ So(extensionList.Extensions[0].Endpoints, ShouldContain, constants.FullMgmt)
+ // Verify the endpoints below are not enabled since trust is not enabled
+ So(extensionList.Extensions[0].Endpoints, ShouldNotContain, constants.FullCosign)
+ So(extensionList.Extensions[0].Endpoints, ShouldNotContain, constants.FullNotation)
+ // Verify the endpoints below are not enabled since the UI is not enabled
+ So(extensionList.Extensions[0].Endpoints, ShouldNotContain, constants.FullUserPrefs)
+ })
- // 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)
+ Convey("start zot server with no enabled extensions", t, func(c C) {
+ conf := config.New()
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
- // delete blob should get 403 without delete perm
- resp, err = client.R().Delete(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/" + digest)
+ conf.HTTP.Port = port
+
+ logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
+ conf.Log.Output = logFile.Name()
+ defer os.Remove(logFile.Name()) // clean up
- // 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
+ ctlr := makeController(conf, t.TempDir())
- // 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)
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- // 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
+ var extensionList distext.ExtensionList
- // 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")
+ resp, err := resty.R().Get(baseURL + constants.RoutePrefix + constants.ExtOciDiscoverPrefix)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
+ So(resp.StatusCode(), ShouldEqual, 200)
+ err = json.Unmarshal(resp.Body(), &extensionList)
+ So(err, ShouldBeNil)
+ t.Log(extensionList.Extensions)
+ // Verify all endpoints which are disabled (even signing urls depend on search being enabled)
+ So(len(extensionList.Extensions), ShouldEqual, 0)
+ })
- // add read perm on repo
- conf.HTTP.AccessControl.Repositories["zot-test"] = config.PolicyGroup{Policies: []config.Policy{
- {
- Users: []string{"test"},
- Actions: []string{"read"},
- },
- }, DefaultPolicy: []string{}}
+ Convey("start minimal zot server", t, func(c C) {
+ conf := config.New()
+ port := test.GetFreePort()
+ baseURL := test.GetBaseURL(port)
- /* 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)
+ conf.HTTP.Port = port
+
+ logFile, err := os.CreateTemp("", "zot-log*.txt")
So(err, ShouldBeNil)
+ conf.Log.Output = logFile.Name()
+ defer os.Remove(logFile.Name()) // clean up
- catalog := struct {
- Repositories []string `json:"repositories"`
- }{}
+ ctlr := makeController(conf, t.TempDir())
- 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)
+ cm := test.NewControllerManager(ctlr)
+ cm.StartAndWait(port)
+ defer cm.StopServer()
- // get manifest should get 200 now
- resp, err = client.R().Get(baseURL + "/v2/zot-test/manifests/0.0.1")
+ var extensionList distext.ExtensionList
+ resp, err := resty.R().Get(baseURL + constants.RoutePrefix + constants.ExtOciDiscoverPrefix)
So(err, ShouldBeNil)
So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
-
- manifestBlob := resp.Body()
- var manifest ispec.Manifest
+ So(resp.StatusCode(), ShouldEqual, 200)
- err = json.Unmarshal(manifestBlob, &manifest)
+ err = json.Unmarshal(resp.Body(), &extensionList)
So(err, ShouldBeNil)
+ So(len(extensionList.Extensions), ShouldEqual, 0)
+ })
+}
- // 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)
+func TestHTTPOptionsResponse(t *testing.T) {
+ Convey("Test http options response", t, func() {
+ conf := config.New()
+ port := test.GetFreePort()
+ conf.HTTP.Port = port
+ baseURL := test.GetBaseURL(port)
- // 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
+ ctlr := api.NewController(conf)
- // 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)
+ firstDir := t.TempDir()
- // create update config and post it.
- cblob, cdigest := GetRandomImageConfig()
+ secondDir := t.TempDir()
+ defer os.RemoveAll(firstDir)
+ defer os.RemoveAll(secondDir)
- 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)
+ ctlr.Config.Storage.RootDirectory = firstDir
+ subPaths := make(map[string]config.StorageConfig)
+ subPaths["/a"] = config.StorageConfig{
+ RootDirectory: secondDir,
+ }
- // 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)
+ ctlr.Config.Storage.SubPaths = subPaths
+ ctrlManager := test.NewControllerManager(ctlr)
- // create updated layer and post it
- updateBlob := []byte("Hello, blob update!")
+ ctrlManager.StartAndWait(port)
- resp, err = client.R().Post(baseURL + "/v2/zot-test/blobs/uploads/")
- So(err, ShouldBeNil)
+ resp, _ := resty.R().Options(baseURL + constants.RoutePrefix + constants.ExtCatalogPrefix)
So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
- loc = test.Location(baseURL, resp)
+ So(resp.StatusCode(), ShouldEqual, http.StatusNoContent)
- // 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)
+ defer ctrlManager.StopServer()
+ })
+}
- 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)),
+func TestGetGithubUserInfo(t *testing.T) {
+ Convey("github api calls works", t, func() {
+ mockedHTTPClient := mock.NewMockedHTTPClient(
+ mock.WithRequestMatch(
+ mock.GetUserEmails,
+ []github.UserEmail{
+ {
+ Email: github.String("test@test"),
+ Primary: github.Bool(true),
+ },
},
- },
- }
- updatedManifest.SchemaVersion = 2
- updatedManifestBlob, err := json.Marshal(updatedManifest)
- So(err, ShouldBeNil)
+ ),
+ mock.WithRequestMatch(
+ mock.GetUserOrgs,
+ []github.Organization{
+ {
+ Login: github.String("testOrg"),
+ },
+ },
+ ),
+ )
- // 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)
+ client := github.NewClient(mockedHTTPClient)
- // get the manifest and check if it's the old one
- resp, err = client.R().Get(baseURL + "/v2/zot-test/manifests/0.0.2")
+ _, _, err := api.GetGithubUserInfo(context.Background(), client, log.Logger{})
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
+ Convey("github ListEmails error", t, func() {
+ mockedHTTPClient := mock.NewMockedHTTPClient(
+ mock.WithRequestMatchHandler(
+ mock.GetUserEmails,
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ mock.WriteError(
+ w,
+ http.StatusInternalServerError,
+ "github error",
+ )
+ }),
+ ),
+ )
- // 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)
+ client := github.NewClient(mockedHTTPClient)
- // 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)
+ _, _, err := api.GetGithubUserInfo(context.Background(), client, log.Logger{})
+ So(err, ShouldNotBeNil)
+ })
- // 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
+ Convey("github ListEmails error", t, func() {
+ mockedHTTPClient := mock.NewMockedHTTPClient(
+ mock.WithRequestMatch(
+ mock.GetUserEmails,
+ []github.UserEmail{
+ {
+ Email: github.String("test@test"),
+ Primary: github.Bool(true),
+ },
+ },
+ ),
+ mock.WithRequestMatchHandler(
+ mock.GetUserOrgs,
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ mock.WriteError(
+ w,
+ http.StatusInternalServerError,
+ "github error",
+ )
+ }),
+ ),
+ )
- resp, err = client.R().Get(baseURL + "/v2/" + AuthorizationNamespace + "/tags/list")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusOK)
+ client := github.NewClient(mockedHTTPClient)
- // upload blob without user create but with default create should get 200
- repoPolicy.DefaultPolicy = append(repoPolicy.DefaultPolicy, "create")
- conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy
+ _, _, err := api.GetGithubUserInfo(context.Background(), client, log.Logger{})
+ So(err, ShouldNotBeNil)
+ })
+}
- resp, err = client.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
+func getAllBlobs(imagePath string) []string {
+ blobList := make([]string, 0)
- // remove per repo policy
- repoPolicy = conf.HTTP.AccessControl.Repositories[AuthorizationNamespace]
- repoPolicy.Policies = []config.Policy{}
- repoPolicy.DefaultPolicy = []string{}
- conf.HTTP.AccessControl.Repositories[AuthorizationNamespace] = repoPolicy
+ if !common.DirExists(imagePath) {
+ return []string{}
+ }
- repoPolicy = conf.HTTP.AccessControl.Repositories["zot-test"]
- repoPolicy.Policies = []config.Policy{}
- repoPolicy.DefaultPolicy = []string{}
- conf.HTTP.AccessControl.Repositories["zot-test"] = repoPolicy
+ buf, err := os.ReadFile(path.Join(imagePath, "index.json"))
+ if err != nil {
+ panic(err)
+ }
- resp, err = client.R().Post(baseURL + "/v2/" + AuthorizationNamespace + "/blobs/uploads/")
- So(err, ShouldBeNil)
- So(resp, ShouldNotBeNil)
- So(resp.StatusCode(), ShouldEqual, http.StatusForbidden)
+ var index ispec.Index
+ if err := json.Unmarshal(buf, &index); err != nil {
+ panic(err)
+ }
- // 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)
+ var digest godigest.Digest
- // 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")
+ for _, m := range index.Manifests {
+ digest = m.Digest
+ blobList = append(blobList, digest.Encoded())
+ p := path.Join(imagePath, "blobs", digest.Algorithm().String(), digest.Encoded())
- // 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)
+ buf, err = os.ReadFile(p)
- // 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)
+ if err != nil {
+ panic(err)
+ }
- // add create perm
- conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "create")
+ var manifest ispec.Manifest
+ if err := json.Unmarshal(buf, &manifest); err != nil {
+ panic(err)
+ }
- // 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")
+ blobList = append(blobList, manifest.Config.Digest.Encoded())
- // 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)
+ for _, layer := range manifest.Layers {
+ blobList = append(blobList, layer.Digest.Encoded())
+ }
+ }
- // 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)
+ return blobList
+}
- // add delete perm
- conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "delete")
+func getAllManifests(imagePath string) []string {
+ manifestList := make([]string, 0)
- // 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)
+ if !common.DirExists(imagePath) {
+ return []string{}
+ }
- // 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)
+ buf, err := os.ReadFile(path.Join(imagePath, "index.json"))
+ if err != nil {
+ panic(err)
+ }
- // add update perm
- conf.HTTP.AccessControl.AdminPolicy.Actions = append(conf.HTTP.AccessControl.AdminPolicy.Actions, "update")
+ var index ispec.Index
+ if err := json.Unmarshal(buf, &index); err != nil {
+ panic(err)
+ }
- // 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)
+ var digest godigest.Digest
- conf.HTTP.AccessControl = &config.AccessControlConfig{}
+ for _, m := range index.Manifests {
+ digest = m.Digest
+ manifestList = append(manifestList, digest.Encoded())
+ }
- 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)
- })
+ return manifestList
+}
+
+func makeController(conf *config.Config, dir string) *api.Controller {
+ ctlr := api.NewController(conf)
+
+ ctlr.Config.Storage.RootDirectory = dir
+
+ return ctlr
}
func getEmptyImageConfig() ([]byte, godigest.Digest) {
diff --git a/pkg/api/routes.go b/pkg/api/routes.go
index 1db9a8044b..6cd5d9a73b 100644
--- a/pkg/api/routes.go
+++ b/pkg/api/routes.go
@@ -183,7 +183,7 @@ func (rh *RouteHandler) SetupRoutes() {
pprof.SetupPprofRoutes(rh.c.Config, prefixedRouter, authHandler, rh.c.Log)
// Preconditions for enabling the actual extension routes are part of extensions themselves
- ext.SetupMetricsRoutes(rh.c.Config, rh.c.Router, authHandler, rh.c.Log, rh.c.Metrics)
+ ext.SetupMetricsRoutes(rh.c.Config, rh.c.Router, authHandler, MetricsAuthzHandler(rh.c), rh.c.Log, rh.c.Metrics)
ext.SetupSearchRoutes(rh.c.Config, prefixedRouter, rh.c.StoreController, rh.c.MetaDB, rh.c.CveScanner,
rh.c.Log)
ext.SetupImageTrustRoutes(rh.c.Config, prefixedRouter, rh.c.MetaDB, rh.c.Log)
diff --git a/pkg/extensions/README.md b/pkg/extensions/README.md
index a6cb9ba33a..6f9df76ba3 100644
--- a/pkg/extensions/README.md
+++ b/pkg/extensions/README.md
@@ -30,9 +30,9 @@ package extensions
IsAdmin bool
Username string
Groups []string
- }
+ }
```
- This data can then be accessed from the request context so that every extension can apply its own authorization logic, if needed .
+ This data can then be accessed from the request context so that every extension can apply its own authorization logic, if needed .
- when a new extension comes out, the developer should also write some blackbox tests, where a binary that contains the new extension should be tested in a real usage scenario. See [test/blackbox](test/blackbox/sync.bats) folder for multiple extensions examples.
@@ -40,6 +40,6 @@ package extensions
- with every new extension, you should modify the EXTENSIONS variable in Makefile by adding the new extension. The EXTENSIONS variable represents all extensions and is used in Make targets that require them all (e.g make test).
-- the available extensions that can be used at the moment are: sync, scrub, metrics, search .
+- the available extensions that can be used at the moment are: sync, search, scrub, metrics, lint, ui, mgmt, userprefs, imagetrust .
NOTE: When multiple extensions are used, they should be listed in the above presented order.
diff --git a/pkg/extensions/extension_metrics.go b/pkg/extensions/extension_metrics.go
index 54e71982fa..1837d7ef78 100644
--- a/pkg/extensions/extension_metrics.go
+++ b/pkg/extensions/extension_metrics.go
@@ -26,13 +26,14 @@ func EnableMetricsExtension(config *config.Config, log log.Logger, rootDir strin
}
func SetupMetricsRoutes(config *config.Config, router *mux.Router,
- authFunc mux.MiddlewareFunc, log log.Logger, metrics monitoring.MetricServer,
+ authnFunc, authzFunc mux.MiddlewareFunc, log log.Logger, metrics monitoring.MetricServer,
) {
log.Info().Msg("setting up metrics routes")
if config.IsMetricsEnabled() {
extRouter := router.PathPrefix(config.Extensions.Metrics.Prometheus.Path).Subrouter()
- extRouter.Use(authFunc)
+ extRouter.Use(authnFunc)
+ extRouter.Use(authzFunc)
extRouter.Methods("GET").Handler(promhttp.Handler())
}
}
diff --git a/pkg/extensions/extension_metrics_disabled.go b/pkg/extensions/extension_metrics_disabled.go
index 6d280b9ba6..624fc06125 100644
--- a/pkg/extensions/extension_metrics_disabled.go
+++ b/pkg/extensions/extension_metrics_disabled.go
@@ -22,13 +22,14 @@ func EnableMetricsExtension(config *config.Config, log log.Logger, rootDir strin
// SetupMetricsRoutes ...
func SetupMetricsRoutes(conf *config.Config, router *mux.Router,
- authFunc mux.MiddlewareFunc, log log.Logger, metrics monitoring.MetricServer,
+ authnFunc, authzFunc mux.MiddlewareFunc, log log.Logger, metrics monitoring.MetricServer,
) {
getMetrics := func(w http.ResponseWriter, r *http.Request) {
m := metrics.ReceiveMetrics()
zcommon.WriteJSON(w, http.StatusOK, m)
}
- router.Use(authFunc)
+ router.Use(authnFunc)
+ router.Use(authzFunc)
router.HandleFunc("/metrics", getMetrics).Methods("GET")
}
diff --git a/pkg/test/skip/skip_test.go b/pkg/test/skip/skip_test.go
new file mode 100644
index 0000000000..b266f11cdb
--- /dev/null
+++ b/pkg/test/skip/skip_test.go
@@ -0,0 +1,37 @@
+package skip_test
+
+import (
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ tskip "zotregistry.io/zot/pkg/test/skip"
+)
+
+// for code coverage.
+func TestSkipS3(t *testing.T) {
+ envName := "S3MOCK_ENDPOINT"
+ envVal := os.Getenv(envName)
+
+ if len(envVal) > 0 {
+ defer os.Setenv(envName, envVal)
+ err := os.Unsetenv(envName)
+ assert.Equal(t, err, nil, "Error should be nil")
+ }
+
+ tskip.SkipS3(t)
+}
+
+func TestSkipDynamo(t *testing.T) {
+ envName := "DYNAMODBMOCK_ENDPOINT"
+ envVal := os.Getenv(envName)
+
+ if len(envVal) > 0 {
+ defer os.Setenv(envName, envVal)
+ err := os.Unsetenv(envName)
+ assert.Equal(t, err, nil, "Error should be nil")
+ }
+
+ tskip.SkipDynamo(t)
+}
diff --git a/test/blackbox/helpers_metrics.bash b/test/blackbox/helpers_metrics.bash
index cce55e5b84..4c225568b8 100644
--- a/test/blackbox/helpers_metrics.bash
+++ b/test/blackbox/helpers_metrics.bash
@@ -1,3 +1,6 @@
+METRICS_USER=observability
+METRICS_PASS=MySecreTPa55
+
function metrics_route_check () {
local servername="http://127.0.0.1:${1}/metrics"
status_code=$(curl --write-out '%{http_code}' ${2} --silent --output /dev/null ${servername})
diff --git a/test/blackbox/metrics.bats b/test/blackbox/metrics.bats
index 0d392ac2cf..9a39c664dd 100644
--- a/test/blackbox/metrics.bats
+++ b/test/blackbox/metrics.bats
@@ -32,6 +32,7 @@ function setup_file() {
zot_config_file=${BATS_FILE_TMPDIR}/zot_config.json
zot_htpasswd_file=${BATS_FILE_TMPDIR}/zot_htpasswd
htpasswd -Bbn ${AUTH_USER} ${AUTH_PASS} >> ${zot_htpasswd_file}
+ htpasswd -Bbn ${METRICS_USER} ${METRICS_PASS} >> ${zot_htpasswd_file}
mkdir -p ${zot_root_dir}
touch ${zot_log_file}
@@ -48,6 +49,20 @@ function setup_file() {
"htpasswd": {
"path": "${zot_htpasswd_file}"
}
+ },
+ "accessControl": {
+ "metrics":{
+ "users": ["${METRICS_USER}"]
+ },
+ "repositories": {
+ "**": {
+ "anonymousPolicy": [
+ "read",
+ "create"
+ ],
+ "defaultPolicy": ["read"]
+ }
+ }
}
},
"log": {
@@ -80,14 +95,20 @@ function teardown_file() {
}
@test "unauthorized request to metrics" {
+# anonymous policy: metrics endpoint should not be available
+# 401 - http.StatusUnauthorized
run metrics_route_check 8080 "" 401
[ "$status" -eq 0 ]
+# user is not in htpasswd
run metrics_route_check 8080 "-u unlucky:wrongpass" 401
[ "$status" -eq 0 ]
+# proper user/pass tuple from htpasswd, but user not allowed to access metrics
+# 403 - http.StatusForbidden
+ run metrics_route_check 8080 "-u ${AUTH_USER}:${AUTH_PASS}" 403
+ [ "$status" -eq 0 ]
}
@test "authorized request: metrics enabled" {
- run metrics_route_check 8080 "-u ${AUTH_USER}:${AUTH_PASS}" 200
+ run metrics_route_check 8080 "-u ${METRICS_USER}:${METRICS_PASS}" 200
[ "$status" -eq 0 ]
}
-
diff --git a/test/blackbox/metrics_minimal.bats b/test/blackbox/metrics_minimal.bats
index c693f64ec5..352498840f 100644
--- a/test/blackbox/metrics_minimal.bats
+++ b/test/blackbox/metrics_minimal.bats
@@ -32,6 +32,7 @@ function setup_file() {
zot_config_file=${BATS_FILE_TMPDIR}/zot_config.json
zot_htpasswd_file=${BATS_FILE_TMPDIR}/zot_htpasswd
htpasswd -Bbn ${AUTH_USER} ${AUTH_PASS} >> ${zot_htpasswd_file}
+ htpasswd -Bbn ${METRICS_USER} ${METRICS_PASS} >> ${zot_htpasswd_file}
mkdir -p ${zot_root_dir}
touch ${zot_log_file}
@@ -48,6 +49,20 @@ function setup_file() {
"htpasswd": {
"path": "${zot_htpasswd_file}"
}
+ },
+ "accessControl": {
+ "metrics":{
+ "users": ["${METRICS_USER}"]
+ },
+ "repositories": {
+ "**": {
+ "anonymousPolicy": [
+ "read",
+ "create"
+ ],
+ "defaultPolicy": ["read"]
+ }
+ }
}
},
"log": {
@@ -72,13 +87,20 @@ function teardown_file() {
}
@test "unauthorized request to metrics" {
+# anonymous policy: metrics endpoint should not be available
+# 401 - http.StatusUnauthorized
run metrics_route_check 8080 "" 401
[ "$status" -eq 0 ]
+# user is not in htpasswd
run metrics_route_check 8080 "-u test:wrongpass" 401
[ "$status" -eq 0 ]
+# proper user/pass tuple from htpasswd, but user not allowed to access metrics
+# 403 - http.StatusForbidden
+ run metrics_route_check 8080 "-u ${AUTH_USER}:${AUTH_PASS}" 403
+ [ "$status" -eq 0 ]
}
@test "authorized request: metrics enabled" {
- run metrics_route_check 8080 "-u ${AUTH_USER}:${AUTH_PASS}" 200
+ run metrics_route_check 8080 "-u ${METRICS_USER}:${METRICS_PASS}" 200
[ "$status" -eq 0 ]
-}
\ No newline at end of file
+}