// +build extended package api_test import ( "bufio" "context" "crypto/tls" "crypto/x509" "encoding/json" "fmt" "io" "io/ioutil" "net" "net/http" "net/http/httptest" "net/url" "os" "path" "regexp" "strings" "testing" "time" "golang.org/x/crypto/bcrypt" "github.com/anuvu/zot/errors" "github.com/anuvu/zot/pkg/api" "github.com/chartmuseum/auth" "github.com/mitchellh/mapstructure" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/phayes/freeport" "github.com/stretchr/testify/assert" vldap "github.com/nmcclain/ldap" . "github.com/smartystreets/goconvey/convey" "gopkg.in/resty.v1" ) const ( BaseURL = "http://127.0.0.1:%s" BaseSecureURL = "https://127.0.0.1:%s" username = "test" passphrase = "test" ServerCert = "../../test/data/server.cert" ServerKey = "../../test/data/server.key" CACert = "../../test/data/ca.crt" AuthorizedNamespace = "everyone/isallowed" UnauthorizedNamespace = "fortknox/notallowed" ALICE = "alice" ) type ( accessTokenResponse struct { AccessToken string `json:"access_token"` } authHeader struct { Realm string Service string Scope string } ) func getFreePort() string { port, err := freeport.GetFreePort() if err != nil { panic(err) } return fmt.Sprint(port) } func getBaseURL(port string, secure bool) string { if secure { return fmt.Sprintf(BaseSecureURL, port) } return fmt.Sprintf(BaseURL, port) } func makeHtpasswdFile() string { f, err := ioutil.TempFile("", "htpasswd-") if err != nil { panic(err) } // bcrypt(username="test", passwd="test") content := []byte("test:$2y$05$hlbSXDp6hzDLu6VwACS39ORvVRpr3OMR4RlJ31jtlaOEGnPjKZI1m\n") if err := ioutil.WriteFile(f.Name(), content, 0600); err != nil { panic(err) } return f.Name() } func makeHtpasswdFileFromString(fileContent string) string { f, err := ioutil.TempFile("", "htpasswd-") if err != nil { panic(err) } // bcrypt(username="test", passwd="test") content := []byte(fileContent) if err := ioutil.WriteFile(f.Name(), content, 0600); err != nil { panic(err) } return f.Name() } func getCredString(username, password string) string { hash, err := bcrypt.GenerateFromPassword([]byte(password), 10) if err != nil { panic(err) } usernameAndHash := fmt.Sprintf("%s:%s", username, string(hash)) return usernameAndHash } func TestNew(t *testing.T) { Convey("Make a new controller", t, func() { config := api.NewConfig() So(config, ShouldNotBeNil) So(api.NewController(config), ShouldNotBeNil) }) } func TestHtpasswdSingleCred(t *testing.T) { Convey("Single cred", t, func() { port := getFreePort() baseURL := getBaseURL(port, false) singleCredtests := []string{} user := ALICE password := ALICE singleCredtests = append(singleCredtests, getCredString(user, password)) singleCredtests = append(singleCredtests, getCredString(user, password)+"\n") for _, testString := range singleCredtests { func() { config := api.NewConfig() config.HTTP.Port = port htpasswdPath := makeHtpasswdFileFromString(testString) defer os.Remove(htpasswdPath) config.HTTP.Auth = &api.AuthConfig{ HTPasswd: api.AuthHTPasswd{ Path: htpasswdPath, }, } c := api.NewController(config) dir, err := ioutil.TempDir("", "oci-repo-test") if err != nil { panic(err) } defer os.RemoveAll(dir) c.Config.Storage.RootDirectory = dir go func(controller *api.Controller) { // this blocks if err := controller.Run(); err != nil { return } }(c) // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } defer func(controller *api.Controller) { ctx := context.Background() _ = controller.Server.Shutdown(ctx) }(c) // with creds, should get expected status code resp, _ := resty.R().SetBasicAuth(user, password).Get(baseURL + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) //with invalid creds, it should fail resp, _ = resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 401) }() } }) } 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 := getFreePort() baseURL := getBaseURL(port, false) config := api.NewConfig() config.HTTP.Port = port htpasswdPath := makeHtpasswdFileFromString(testString) defer os.Remove(htpasswdPath) config.HTTP.Auth = &api.AuthConfig{ HTPasswd: api.AuthHTPasswd{ Path: htpasswdPath, }, } c := api.NewController(config) dir, err := ioutil.TempDir("", "oci-repo-test") if err != nil { panic(err) } defer os.RemoveAll(dir) c.Config.Storage.RootDirectory = dir go func(controller *api.Controller) { // this blocks if err := controller.Run(); err != nil { return } }(c) // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } defer func(controller *api.Controller) { ctx := context.Background() _ = controller.Server.Shutdown(ctx) }(c) // with creds, should get expected status code resp, _ := resty.R().SetBasicAuth(user1, password1).Get(baseURL + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) resp, _ = resty.R().SetBasicAuth(user2, password2).Get(baseURL + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) //with invalid creds, it should fail resp, _ = resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 401) }() } }) } 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 := getFreePort() baseURL := getBaseURL(port, false) config := api.NewConfig() config.HTTP.Port = port htpasswdPath := makeHtpasswdFileFromString(credString.String()) defer os.Remove(htpasswdPath) config.HTTP.Auth = &api.AuthConfig{ HTPasswd: api.AuthHTPasswd{ Path: htpasswdPath, }, } c := api.NewController(config) dir, err := ioutil.TempDir("", "oci-repo-test") if err != nil { panic(err) } defer os.RemoveAll(dir) c.Config.Storage.RootDirectory = dir go func(controller *api.Controller) { // this blocks if err := controller.Run(); err != nil { return } }(c) // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } defer func(controller *api.Controller) { ctx := context.Background() _ = controller.Server.Shutdown(ctx) }(c) // 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, 200) } //with invalid creds, it should fail resp, _ := resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 401) }() }) } func TestBasicAuth(t *testing.T) { Convey("Make a new controller", t, func() { port := getFreePort() baseURL := getBaseURL(port, false) config := api.NewConfig() config.HTTP.Port = port htpasswdPath := makeHtpasswdFile() defer os.Remove(htpasswdPath) config.HTTP.Auth = &api.AuthConfig{ HTPasswd: api.AuthHTPasswd{ Path: htpasswdPath, }, } c := api.NewController(config) dir, err := ioutil.TempDir("", "oci-repo-test") if err != nil { panic(err) } defer os.RemoveAll(dir) c.Config.Storage.RootDirectory = dir go func() { // this blocks if err := c.Run(); err != nil { return } }() // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } defer func() { ctx := context.Background() _ = c.Server.Shutdown(ctx) }() // without creds, should get access error resp, err := resty.R().Get(baseURL + "/v2/") So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 401) var e api.Error err = json.Unmarshal(resp.Body(), &e) 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, 404) resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) }) } func TestMultipleInstance(t *testing.T) { Convey("Negative test zot multiple instance", t, func() { port := getFreePort() baseURL := getBaseURL(port, false) config := api.NewConfig() config.HTTP.Port = port htpasswdPath := makeHtpasswdFile() defer os.Remove(htpasswdPath) config.HTTP.Auth = &api.AuthConfig{ HTPasswd: api.AuthHTPasswd{ Path: htpasswdPath, }, } c := api.NewController(config) err := c.Run() So(err, ShouldEqual, errors.ErrImgStoreNotFound) globalDir, err := ioutil.TempDir("", "oci-repo-test") if err != nil { panic(err) } defer os.RemoveAll(globalDir) subDir, err := ioutil.TempDir("/tmp", "oci-sub-test") if err != nil { panic(err) } defer os.RemoveAll(subDir) c.Config.Storage.RootDirectory = globalDir subPathMap := make(map[string]api.StorageConfig) subPathMap["/a"] = api.StorageConfig{RootDirectory: subDir} go func() { if err := c.Run(); err != nil { return } }() // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } defer func() { ctx := context.Background() _ = c.Server.Shutdown(ctx) }() client := resty.New() tagResponse, err := client.R().SetBasicAuth(username, passphrase). Get(baseURL + "/v2/zot-test/tags/list") So(err, ShouldBeNil) So(tagResponse.StatusCode(), ShouldEqual, 404) }) Convey("Test zot multiple instance", t, func() { port := getFreePort() baseURL := getBaseURL(port, false) config := api.NewConfig() config.HTTP.Port = port htpasswdPath := makeHtpasswdFile() defer os.Remove(htpasswdPath) config.HTTP.Auth = &api.AuthConfig{ HTPasswd: api.AuthHTPasswd{ Path: htpasswdPath, }, } c := api.NewController(config) globalDir, err := ioutil.TempDir("", "oci-repo-test") if err != nil { panic(err) } defer os.RemoveAll(globalDir) subDir, err := ioutil.TempDir("/tmp", "oci-sub-test") if err != nil { panic(err) } defer os.RemoveAll(subDir) c.Config.Storage.RootDirectory = globalDir subPathMap := make(map[string]api.StorageConfig) subPathMap["/a"] = api.StorageConfig{RootDirectory: subDir} go func() { // this blocks if err := c.Run(); err != nil { return } }() // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } defer func() { ctx := context.Background() _ = c.Server.Shutdown(ctx) }() // without creds, should get access error resp, err := resty.R().Get(baseURL + "/v2/") So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 401) var e api.Error err = json.Unmarshal(resp.Body(), &e) 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, 404) resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) }) } func TestTLSWithBasicAuth(t *testing.T) { Convey("Make a new controller", t, func() { caCert, err := ioutil.ReadFile(CACert) So(err, ShouldBeNil) caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) htpasswdPath := makeHtpasswdFile() defer os.Remove(htpasswdPath) port := getFreePort() baseURL := getBaseURL(port, false) secureBaseURL := getBaseURL(port, true) resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool}) defer func() { resty.SetTLSClientConfig(nil) }() config := api.NewConfig() config.HTTP.Port = port config.HTTP.TLS = &api.TLSConfig{ Cert: ServerCert, Key: ServerKey, } config.HTTP.Auth = &api.AuthConfig{ HTPasswd: api.AuthHTPasswd{ Path: htpasswdPath, }, } c := api.NewController(config) dir, err := ioutil.TempDir("", "oci-repo-test") if err != nil { panic(err) } defer os.RemoveAll(dir) c.Config.Storage.RootDirectory = dir go func() { // this blocks if err := c.Run(); err != nil { return } }() // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } defer func() { ctx := context.Background() _ = c.Server.Shutdown(ctx) }() // accessing insecure HTTP site should fail resp, err := resty.R().Get(baseURL) So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 400) // without creds, should get access error resp, err = resty.R().Get(secureBaseURL + "/v2/") So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 401) var e api.Error err = json.Unmarshal(resp.Body(), &e) So(err, ShouldBeNil) // with creds, should get expected status code resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 404) resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) }) } func TestTLSWithBasicAuthAllowReadAccess(t *testing.T) { Convey("Make a new controller", t, func() { caCert, err := ioutil.ReadFile(CACert) So(err, ShouldBeNil) caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) htpasswdPath := makeHtpasswdFile() defer os.Remove(htpasswdPath) port := getFreePort() baseURL := getBaseURL(port, false) secureBaseURL := getBaseURL(port, true) resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool}) defer func() { resty.SetTLSClientConfig(nil) }() config := api.NewConfig() config.HTTP.Port = port config.HTTP.Auth = &api.AuthConfig{ HTPasswd: api.AuthHTPasswd{ Path: htpasswdPath, }, } config.HTTP.TLS = &api.TLSConfig{ Cert: ServerCert, Key: ServerKey, } config.HTTP.AllowReadAccess = true c := api.NewController(config) dir, err := ioutil.TempDir("", "oci-repo-test") if err != nil { panic(err) } defer os.RemoveAll(dir) c.Config.Storage.RootDirectory = dir go func() { // this blocks if err := c.Run(); err != nil { return } }() // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } defer func() { ctx := context.Background() _ = c.Server.Shutdown(ctx) }() // accessing insecure HTTP site should fail resp, err := resty.R().Get(baseURL) So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 400) // without creds, should still be allowed to access resp, err = resty.R().Get(secureBaseURL + "/v2/") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) // with creds, should get expected status code resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 404) resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) // without creds, writes should fail resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 401) }) } func TestTLSMutualAuth(t *testing.T) { Convey("Make a new controller", t, func() { caCert, err := ioutil.ReadFile(CACert) So(err, ShouldBeNil) caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) port := getFreePort() baseURL := getBaseURL(port, false) secureBaseURL := getBaseURL(port, true) resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool}) defer func() { resty.SetTLSClientConfig(nil) }() config := api.NewConfig() config.HTTP.Port = port config.HTTP.TLS = &api.TLSConfig{ Cert: ServerCert, Key: ServerKey, CACert: CACert, } c := api.NewController(config) dir, err := ioutil.TempDir("", "oci-repo-test") if err != nil { panic(err) } defer os.RemoveAll(dir) c.Config.Storage.RootDirectory = dir go func() { // this blocks if err := c.Run(); err != nil { return } }() // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } defer func() { ctx := context.Background() _ = c.Server.Shutdown(ctx) }() // accessing insecure HTTP site should fail resp, err := resty.R().Get(baseURL) So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 400) // 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, 200) // 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, 404) // with client certs, creds shouldn't matter resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) }) } func TestTLSMutualAuthAllowReadAccess(t *testing.T) { Convey("Make a new controller", t, func() { caCert, err := ioutil.ReadFile(CACert) So(err, ShouldBeNil) caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) port := getFreePort() baseURL := getBaseURL(port, false) secureBaseURL := getBaseURL(port, true) resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool}) defer func() { resty.SetTLSClientConfig(nil) }() config := api.NewConfig() config.HTTP.Port = port config.HTTP.TLS = &api.TLSConfig{ Cert: ServerCert, Key: ServerKey, CACert: CACert, } config.HTTP.AllowReadAccess = true c := api.NewController(config) dir, err := ioutil.TempDir("", "oci-repo-test") if err != nil { panic(err) } defer os.RemoveAll(dir) c.Config.Storage.RootDirectory = dir go func() { // this blocks if err := c.Run(); err != nil { return } }() // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } defer func() { ctx := context.Background() _ = c.Server.Shutdown(ctx) }() // accessing insecure HTTP site should fail resp, err := resty.R().Get(baseURL) So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 400) // without client certs and creds, reads are allowed resp, err = resty.R().Get(secureBaseURL + "/v2/") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) // 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, 200) // without creds, writes should fail resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 401) // 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, 200) // 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, 404) // with client certs, creds shouldn't matter resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) }) } func TestTLSMutualAndBasicAuth(t *testing.T) { Convey("Make a new controller", t, func() { caCert, err := ioutil.ReadFile(CACert) So(err, ShouldBeNil) caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) htpasswdPath := makeHtpasswdFile() defer os.Remove(htpasswdPath) port := getFreePort() baseURL := getBaseURL(port, false) secureBaseURL := getBaseURL(port, true) resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool}) defer func() { resty.SetTLSClientConfig(nil) }() config := api.NewConfig() config.HTTP.Port = port config.HTTP.Auth = &api.AuthConfig{ HTPasswd: api.AuthHTPasswd{ Path: htpasswdPath, }, } config.HTTP.TLS = &api.TLSConfig{ Cert: ServerCert, Key: ServerKey, CACert: CACert, } c := api.NewController(config) dir, err := ioutil.TempDir("", "oci-repo-test") if err != nil { panic(err) } defer os.RemoveAll(dir) c.Config.Storage.RootDirectory = dir go func() { // this blocks if err := c.Run(); err != nil { return } }() // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } defer func() { ctx := context.Background() _ = c.Server.Shutdown(ctx) }() // accessing insecure HTTP site should fail resp, err := resty.R().Get(baseURL) So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 400) // without client certs and creds, should fail _, err = resty.R().Get(secureBaseURL) So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 400) // 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, 400) // 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, 401) // 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, 404) resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) }) } func TestTLSMutualAndBasicAuthAllowReadAccess(t *testing.T) { Convey("Make a new controller", t, func() { caCert, err := ioutil.ReadFile(CACert) So(err, ShouldBeNil) caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) htpasswdPath := makeHtpasswdFile() defer os.Remove(htpasswdPath) port := getFreePort() baseURL := getBaseURL(port, false) secureBaseURL := getBaseURL(port, true) resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool}) defer func() { resty.SetTLSClientConfig(nil) }() config := api.NewConfig() config.HTTP.Port = port config.HTTP.Auth = &api.AuthConfig{ HTPasswd: api.AuthHTPasswd{ Path: htpasswdPath, }, } config.HTTP.TLS = &api.TLSConfig{ Cert: ServerCert, Key: ServerKey, CACert: CACert, } config.HTTP.AllowReadAccess = true c := api.NewController(config) dir, err := ioutil.TempDir("", "oci-repo-test") if err != nil { panic(err) } defer os.RemoveAll(dir) c.Config.Storage.RootDirectory = dir go func() { // this blocks if err := c.Run(); err != nil { return } }() // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } defer func() { ctx := context.Background() _ = c.Server.Shutdown(ctx) }() // accessing insecure HTTP site should fail resp, err := resty.R().Get(baseURL) So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 400) // without client certs and creds, should fail _, err = resty.R().Get(secureBaseURL) So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 400) // 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, 400) // 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, 200) // with only client certs, writes should fail resp, err = resty.R().Post(secureBaseURL + "/v2/repo/blobs/uploads/") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 401) // 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, 404) resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(secureBaseURL + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) }) } const ( LDAPAddress = "127.0.0.1" LDAPPort = 9636 LDAPBaseDN = "ou=test" LDAPBindDN = "cn=reader," + LDAPBaseDN LDAPBindPassword = "bindPassword" ) type testLDAPServer struct { server *vldap.Server quitCh chan bool } func newTestLDAPServer() *testLDAPServer { l := &testLDAPServer{} quitCh := make(chan bool) server := vldap.NewServer() server.QuitChannel(quitCh) server.BindFunc("", l) server.SearchFunc("", l) l.server = server l.quitCh = quitCh return l } func (l *testLDAPServer) Start() { addr := fmt.Sprintf("%s:%d", LDAPAddress, LDAPPort) 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)}, }, ResultCode: vldap.LDAPResultSuccess, }, nil } return vldap.ServerSearchResult{}, nil } func TestBasicAuthWithLDAP(t *testing.T) { Convey("Make a new controller", t, func() { l := newTestLDAPServer() l.Start() defer l.Stop() port := getFreePort() baseURL := getBaseURL(port, false) config := api.NewConfig() config.HTTP.Port = port config.HTTP.Auth = &api.AuthConfig{ LDAP: &api.LDAPConfig{ Insecure: true, Address: LDAPAddress, Port: LDAPPort, BindDN: LDAPBindDN, BindPassword: LDAPBindPassword, BaseDN: LDAPBaseDN, UserAttribute: "uid", }, } c := api.NewController(config) dir, err := ioutil.TempDir("", "oci-repo-test") if err != nil { panic(err) } defer os.RemoveAll(dir) c.Config.Storage.RootDirectory = dir go func() { // this blocks if err := c.Run(); err != nil { return } }() // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } defer func() { ctx := context.Background() _ = c.Server.Shutdown(ctx) }() // without creds, should get access error resp, err := resty.R().Get(baseURL + "/v2/") So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 401) var e api.Error err = json.Unmarshal(resp.Body(), &e) 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, 404) resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(baseURL + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) }) } func TestBearerAuth(t *testing.T) { Convey("Make a new controller", t, func() { authTestServer := makeAuthTestServer() defer authTestServer.Close() port := getFreePort() baseURL := getBaseURL(port, false) config := api.NewConfig() config.HTTP.Port = port u, err := url.Parse(authTestServer.URL) So(err, ShouldBeNil) config.HTTP.Auth = &api.AuthConfig{ Bearer: &api.BearerConfig{ Cert: ServerCert, Realm: authTestServer.URL + "/auth/token", Service: u.Host, }, } c := api.NewController(config) dir, err := ioutil.TempDir("", "oci-repo-test") So(err, ShouldBeNil) defer os.RemoveAll(dir) c.Config.Storage.RootDirectory = dir go func() { // this blocks if err := c.Run(); err != nil { return } }() // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } defer func() { ctx := context.Background() _ = c.Server.Shutdown(ctx) }() 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, 401) authorizationHeader := 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, 200) var goodToken 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, 200) resp, err = resty.R().Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/") So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 401) authorizationHeader = 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, 200) 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, 202) 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, 401) authorizationHeader = 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, 200) 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, 201) 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, 401) authorizationHeader = 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, 200) 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, 200) resp, err = resty.R(). Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/") So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 401) authorizationHeader = 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, 200) var badToken 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, 401) }) } func TestBearerAuthWithAllowReadAccess(t *testing.T) { Convey("Make a new controller", t, func() { authTestServer := makeAuthTestServer() defer authTestServer.Close() port := getFreePort() baseURL := getBaseURL(port, false) config := api.NewConfig() config.HTTP.Port = port u, err := url.Parse(authTestServer.URL) So(err, ShouldBeNil) config.HTTP.Auth = &api.AuthConfig{ Bearer: &api.BearerConfig{ Cert: ServerCert, Realm: authTestServer.URL + "/auth/token", Service: u.Host, }, } config.HTTP.AllowReadAccess = true c := api.NewController(config) dir, err := ioutil.TempDir("", "oci-repo-test") So(err, ShouldBeNil) defer os.RemoveAll(dir) c.Config.Storage.RootDirectory = dir go func() { // this blocks if err := c.Run(); err != nil { return } }() // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } defer func() { ctx := context.Background() _ = c.Server.Shutdown(ctx) }() 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, 401) authorizationHeader := 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, 200) var goodToken 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, 200) resp, err = resty.R().Post(baseURL + "/v2/" + AuthorizedNamespace + "/blobs/uploads/") So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 401) authorizationHeader = 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, 200) 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, 202) 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, 401) authorizationHeader = 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, 200) 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, 201) 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, 401) authorizationHeader = 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, 200) 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, 200) resp, err = resty.R(). Post(baseURL + "/v2/" + UnauthorizedNamespace + "/blobs/uploads/") So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 401) authorizationHeader = 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, 200) var badToken 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, 401) }) } func makeAuthTestServer() *httptest.Server { cmTokenGenerator, err := auth.NewTokenGenerator(&auth.TokenGeneratorOptions{ PrivateKeyPath: ServerKey, Audience: "Zot Registry", Issuer: "Zot", AddKIDHeader: true, }) if err != nil { panic(err) } authTestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { scope := r.URL.Query().Get("scope") parts := strings.Split(scope, ":") name := parts[1] actions := strings.Split(parts[2], ",") if name == UnauthorizedNamespace { actions = []string{} } access := []auth.AccessEntry{ { Name: name, Type: "repository", Actions: actions, }, } token, err := cmTokenGenerator.GenerateToken(access, time.Minute*1) if err != nil { panic(err) } w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"access_token": "%s"}`, token) })) return authTestServer } func parseBearerAuthHeader(authHeaderRaw string) *authHeader { re := regexp.MustCompile(`([a-zA-z]+)="(.+?)"`) matches := re.FindAllStringSubmatch(authHeaderRaw, -1) m := make(map[string]string) for i := 0; i < len(matches); i++ { m[matches[i][1]] = matches[i][2] } var h authHeader if err := mapstructure.Decode(m, &h); err != nil { panic(err) } return &h } func TestInvalidCases(t *testing.T) { Convey("Invalid repo dir", t, func() { port := getFreePort() baseURL := getBaseURL(port, false) config := api.NewConfig() config.HTTP.Port = port htpasswdPath := makeHtpasswdFileFromString(getCredString(username, passphrase)) defer os.Remove(htpasswdPath) config.HTTP.Auth = &api.AuthConfig{ HTPasswd: api.AuthHTPasswd{ Path: htpasswdPath, }, } c := api.NewController(config) err := os.Mkdir("oci-repo-test", 0000) if err != nil { panic(err) } defer stopServer(c) c.Config.Storage.RootDirectory = "oci-repo-test" go func() { // this blocks if err := c.Run(); err != nil { return } }() // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } digest := "sha256:8dd57e171a61368ffcfde38045ddb6ed74a32950c271c1da93eaddfb66a77e78" 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, 500) }) } 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 := getFreePort() baseURL := getBaseURL(port, false) for _, testString := range singleCredtests { func() { config := api.NewConfig() config.HTTP.Port = port // enable read-only mode config.HTTP.ReadOnly = true htpasswdPath := makeHtpasswdFileFromString(testString) defer os.Remove(htpasswdPath) config.HTTP.Auth = &api.AuthConfig{ HTPasswd: api.AuthHTPasswd{ Path: htpasswdPath, }, } c := api.NewController(config) dir, err := ioutil.TempDir("", "oci-repo-test") if err != nil { panic(err) } defer os.RemoveAll(dir) c.Config.Storage.RootDirectory = dir go func(controller *api.Controller) { // this blocks if err := controller.Run(); err != nil { return } }(c) // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } defer func(controller *api.Controller) { ctx := context.Background() _ = controller.Server.Shutdown(ctx) }(c) // with creds, should get expected status code resp, _ := resty.R().SetBasicAuth(user, password).Get(baseURL + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) // 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, 405) //with invalid creds, it should fail resp, _ = resty.R().SetBasicAuth("chuck", "chuck").Get(baseURL + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 401) }() } }) } func TestCrossRepoMount(t *testing.T) { Convey("Cross Repo Mount", t, func() { port := getFreePort() baseURL := getBaseURL(port, false) config := api.NewConfig() config.HTTP.Port = port htpasswdPath := makeHtpasswdFileFromString(getCredString(username, passphrase)) defer os.Remove(htpasswdPath) config.HTTP.Auth = &api.AuthConfig{ HTPasswd: api.AuthHTPasswd{ Path: htpasswdPath, }, } c := api.NewController(config) dir, err := ioutil.TempDir("", "oci-repo-test") if err != nil { panic(err) } err = copyFiles("../../test/data", dir) if err != nil { panic(err) } defer os.RemoveAll(dir) c.Config.Storage.RootDirectory = dir go func() { // this blocks if err := c.Run(); err != nil { return } }() // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } params := make(map[string]string) digest := "sha256:63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e29" d := godigest.Digest(digest) name := "zot-cve-test" params["mount"] = digest params["from"] = name 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, 200) // 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, 202) incorrectParams := make(map[string]string) incorrectParams["mount"] = "sha256:63a795ca90aa6e7dda60941e826810a4cd0a2e73ea02bf458241df2a5c973e29" 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, 202) // Use correct request // This is correct request but it will return 202 because blob is not present in cache. params["mount"] = digest postResponse, err = client.R(). SetBasicAuth(username, passphrase).SetQueryParams(params). Post(baseURL + "/v2/zot-c-test/blobs/uploads/") So(err, ShouldBeNil) So(postResponse.StatusCode(), ShouldEqual, 202) // 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, 202) // 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, 202) headResponse, err = client.R().SetBasicAuth(username, passphrase). Head(fmt.Sprintf("%s/v2/zot-cv-test/blobs/%s", baseURL, digest)) So(err, ShouldBeNil) So(headResponse.StatusCode(), ShouldEqual, 404) postResponse, err = client.R(). SetBasicAuth(username, passphrase).SetQueryParams(params).Post(baseURL + "/v2/zot-c-test/blobs/uploads/") So(err, ShouldBeNil) So(postResponse.StatusCode(), ShouldEqual, 202) postResponse, err = client.R(). SetBasicAuth(username, passphrase).SetQueryParams(params). Post(baseURL + "/v2/ /blobs/uploads/") So(err, ShouldBeNil) So(postResponse.StatusCode(), ShouldEqual, 404) digest = "sha256:63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e29" blob := "63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e29" buf, err := ioutil.ReadFile(path.Join(c.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, 201) // 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. params["mount"] = digest postResponse, err = client.R(). SetBasicAuth(username, passphrase).SetQueryParams(params). Post(baseURL + "/v2/zot-mount-test/blobs/uploads/") So(err, ShouldBeNil) So(postResponse.StatusCode(), ShouldEqual, 201) // Check os.SameFile here cachePath := path.Join(c.Config.Storage.RootDirectory, "zot-d-test", "blobs/sha256", d.Hex()) cacheFi, err := os.Stat(cachePath) So(err, ShouldBeNil) linkPath := path.Join(c.Config.Storage.RootDirectory, "zot-mount-test", "blobs/sha256", d.Hex()) 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"] = digest 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, 201) linkPath = path.Join(c.Config.Storage.RootDirectory, "zot-mount1-test", "blobs/sha256", d.Hex()) 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, digest)) So(err, ShouldBeNil) So(headResponse.StatusCode(), ShouldEqual, 200) // 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, 405) 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, 405) }) Convey("Disable dedupe and cache", t, func() { port := getFreePort() baseURL := getBaseURL(port, false) config := api.NewConfig() config.HTTP.Port = port htpasswdPath := makeHtpasswdFileFromString(getCredString(username, passphrase)) defer os.Remove(htpasswdPath) config.HTTP.Auth = &api.AuthConfig{ HTPasswd: api.AuthHTPasswd{ Path: htpasswdPath, }, } c := api.NewController(config) //defer stopServer(c) dir, err := ioutil.TempDir("", "oci-repo-test") if err != nil { panic(err) } err = copyFiles("../../test/data", dir) if err != nil { panic(err) } defer os.RemoveAll(dir) c.Config.Storage.RootDirectory = dir c.Config.Storage.Dedupe = false c.Config.Storage.GC = false go func() { // this blocks if err := c.Run(); err != nil { return } }() // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } digest := "sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621" 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, 404) }) } func TestParallelRequests(t *testing.T) { 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 := getFreePort() baseURL := getBaseURL(port, false) config := api.NewConfig() config.HTTP.Port = port htpasswdPath := makeHtpasswdFileFromString(getCredString(username, passphrase)) config.HTTP.Auth = &api.AuthConfig{ HTPasswd: api.AuthHTPasswd{ Path: htpasswdPath, }, } c := api.NewController(config) dir, err := ioutil.TempDir("", "oci-repo-test") if err != nil { panic(err) } firstSubDir, err := ioutil.TempDir("", "oci-sub-dir") if err != nil { panic(err) } secondSubDir, err := ioutil.TempDir("", "oci-sub-dir") if err != nil { panic(err) } subPaths := make(map[string]api.StorageConfig) subPaths["/a"] = api.StorageConfig{RootDirectory: firstSubDir} subPaths["/b"] = api.StorageConfig{RootDirectory: secondSubDir} c.Config.Storage.SubPaths = subPaths c.Config.Storage.RootDirectory = dir go func() { // this blocks if err := c.Run(); err != nil { return } }() // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } // without creds, should get access error for i, testcase := range testCases { testcase := testcase j := 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(), 400, "bad request") manifestList := getAllManifests(path.Join("../../test/data", 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(), 404, "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(), 404, "response status code should return 404") } blobList := getAllBlobs(path.Join("../../test/data", 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(), 500, "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(), 500, "internal server error should not occurred") blobPath := path.Join("../../test/data", testcase.srcImageName, "blobs/sha256", blob) buf, err := ioutil.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(), 500, "response status code should not return 500") // Post request with query parameter if j%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(), 500, "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) b := make([]byte, 5*1024*1024) if j%4 == 0 { readContent := 0 for { n, err := reader.Read(b) if err != nil { if err == io.EOF { break } panic(err) } // Patch request of blob patchResponse, err := client.R(). SetBody(b[0:n]). SetHeader("Content-Type", "application/octet-stream"). SetHeader("Content-Length", fmt.Sprintf("%d", n)). SetHeader("Content-Range", fmt.Sprintf("%d", readContent)+"-"+fmt.Sprintf("%d", readContent+n-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(), 500, "response status code should not return 500") readContent += n } } else { for { n, err := reader.Read(b) if err != nil { if err == io.EOF { break } panic(err) } // Patch request of blob patchResponse, err := client.R().SetBody(b[0:n]).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(), 500, "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(), 500, "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(), 500, "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(), 500, "response should return success code") if i < 5 { // nolint: scopelint deleteResponse, err := client.R(). SetBasicAuth(username, passphrase). Delete(baseURL + "/v2/" + testcase.destImageName + "/blobs/sha256:" + blob) assert.Equal(t, err, nil, "Should not be nil") assert.Equal(t, deleteResponse.StatusCode(), 202, "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(), 200, "response status code should return success code") repoResponse, err := client.R().SetBasicAuth(username, passphrase). Get(baseURL + "/v2/_catalog") assert.Equal(t, err, nil, "Error should be nil") assert.Equal(t, repoResponse.StatusCode(), 200, "response status code should return success code") }) } } func getAllBlobs(imagePath string) []string { blobList := make([]string, 0) if !dirExists(imagePath) { return []string{} } buf, err := ioutil.ReadFile(path.Join(imagePath, "index.json")) if err != nil { panic(err) } var index ispec.Index if err := json.Unmarshal(buf, &index); err != nil { panic(err) } var digest godigest.Digest for _, m := range index.Manifests { digest = m.Digest blobList = append(blobList, digest.Encoded()) p := path.Join(imagePath, "blobs", digest.Algorithm().String(), digest.Encoded()) buf, err = ioutil.ReadFile(p) if err != nil { panic(err) } var manifest ispec.Manifest if err := json.Unmarshal(buf, &manifest); err != nil { panic(err) } blobList = append(blobList, manifest.Config.Digest.Encoded()) for _, layer := range manifest.Layers { blobList = append(blobList, layer.Digest.Encoded()) } } return blobList } func getAllManifests(imagePath string) []string { manifestList := make([]string, 0) if !dirExists(imagePath) { return []string{} } buf, err := ioutil.ReadFile(path.Join(imagePath, "index.json")) if err != nil { panic(err) } var index ispec.Index if err := json.Unmarshal(buf, &index); err != nil { panic(err) } var digest godigest.Digest for _, m := range index.Manifests { digest = m.Digest manifestList = append(manifestList, digest.Encoded()) } return manifestList } func dirExists(d string) bool { fi, err := os.Stat(d) if err != nil && os.IsNotExist(err) { return false } if !fi.IsDir() { return false } return true } func copyFiles(sourceDir string, destDir string) error { sourceMeta, err := os.Stat(sourceDir) if err != nil { return err } if err := os.MkdirAll(destDir, sourceMeta.Mode()); err != nil { return err } files, err := ioutil.ReadDir(sourceDir) if err != nil { return err } for _, file := range files { sourceFilePath := path.Join(sourceDir, file.Name()) destFilePath := path.Join(destDir, file.Name()) if file.IsDir() { if err = copyFiles(sourceFilePath, destFilePath); err != nil { return err } } else { sourceFile, err := os.Open(sourceFilePath) if err != nil { return err } defer sourceFile.Close() destFile, err := os.Create(destFilePath) if err != nil { return err } defer destFile.Close() if _, err = io.Copy(destFile, sourceFile); err != nil { return err } } } return nil } func stopServer(ctrl *api.Controller) { err := ctrl.Server.Shutdown(context.Background()) if err != nil { panic(err) } err = os.RemoveAll(ctrl.Config.Storage.RootDirectory) if err != nil { panic(err) } } func TestHardLink(t *testing.T) { Convey("Validate hard link", t, func() { port := getFreePort() baseURL := getBaseURL(port, false) config := api.NewConfig() config.HTTP.Port = port htpasswdPath := makeHtpasswdFileFromString(getCredString(username, passphrase)) config.HTTP.Auth = &api.AuthConfig{ HTPasswd: api.AuthHTPasswd{ Path: htpasswdPath, }, } c := api.NewController(config) dir, err := ioutil.TempDir("", "hard-link-test") if err != nil { panic(err) } defer os.RemoveAll(dir) err = os.Chmod(dir, 0400) if err != nil { panic(err) } subDir, err := ioutil.TempDir("", "sub-hardlink-test") if err != nil { panic(err) } defer os.RemoveAll(subDir) err = os.Chmod(subDir, 0400) if err != nil { panic(err) } c.Config.Storage.RootDirectory = dir subPaths := make(map[string]api.StorageConfig) subPaths["/a"] = api.StorageConfig{RootDirectory: subDir, Dedupe: true} c.Config.Storage.SubPaths = subPaths go func() { // this blocks if err := c.Run(); err != nil { return } }() time.Sleep(5 * time.Second) // wait till ready for { _, err := resty.R().Get(baseURL) if err == nil { break } time.Sleep(100 * time.Millisecond) } err = os.Chmod(dir, 0644) if err != nil { panic(err) } err = os.Chmod(subDir, 0644) if err != nil { panic(err) } So(c.Config.Storage.Dedupe, ShouldEqual, false) }) }