package api_test import ( "context" "crypto/tls" "crypto/x509" "encoding/json" "errors" "fmt" "io/ioutil" "net" "os" "testing" "time" "github.com/anuvu/zot/pkg/api" vldap "github.com/nmcclain/ldap" . "github.com/smartystreets/goconvey/convey" "gopkg.in/resty.v1" ) const ( BaseURL1 = "http://127.0.0.1:8081" BaseURL2 = "http://127.0.0.1:8082" BaseSecureURL2 = "https://127.0.0.1:8082" SecurePort1 = "8081" SecurePort2 = "8082" username = "test" passphrase = "test" htpasswdPath = "../../test/data/htpasswd" // nolint (gosec) - this is just test data ServerCert = "../../test/data/server.cert" ServerKey = "../../test/data/server.key" CACert = "../../test/data/ca.crt" ) func TestNew(t *testing.T) { Convey("Make a new controller", t, func() { config := api.NewConfig() So(config, ShouldNotBeNil) So(api.NewController(config), ShouldNotBeNil) }) } func TestBasicAuth(t *testing.T) { Convey("Make a new controller", t, func() { config := api.NewConfig() config.HTTP.Port = SecurePort1 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(BaseURL1) 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(BaseURL1 + "/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(BaseURL1) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 404) resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseURL1 + "/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) resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool}) defer func() { resty.SetTLSClientConfig(nil) }() config := api.NewConfig() config.HTTP.Port = SecurePort2 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(BaseURL2) 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(BaseURL2) So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 400) // without creds, should get access error resp, err = resty.R().Get(BaseSecureURL2 + "/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(BaseSecureURL2) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 404) resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2 + "/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) resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool}) defer func() { resty.SetTLSClientConfig(nil) }() config := api.NewConfig() config.HTTP.Port = SecurePort2 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(BaseURL2) 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(BaseURL2) So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 400) // without creds, should still be allowed to access resp, err = resty.R().Get(BaseSecureURL2 + "/v2/") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) // with creds, should get expected status code resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 404) resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2 + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) // without creds, writes should fail resp, err = resty.R().Post(BaseSecureURL2 + "/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) resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool}) defer func() { resty.SetTLSClientConfig(nil) }() config := api.NewConfig() config.HTTP.Port = SecurePort2 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(BaseURL2) 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(BaseURL2) So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 400) // without client certs and creds, should get conn error _, err = resty.R().Get(BaseSecureURL2) So(err, ShouldNotBeNil) // with creds but without certs, should get conn error _, err = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2) 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(BaseSecureURL2 + "/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(BaseSecureURL2) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 404) // with client certs, creds shouldn't matter resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2 + "/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) resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool}) defer func() { resty.SetTLSClientConfig(nil) }() config := api.NewConfig() config.HTTP.Port = SecurePort2 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(BaseURL2) 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(BaseURL2) So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 400) // without client certs and creds, reads are allowed resp, err = resty.R().Get(BaseSecureURL2 + "/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(BaseSecureURL2 + "/v2/") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) // without creds, writes should fail resp, err = resty.R().Post(BaseSecureURL2 + "/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(BaseSecureURL2 + "/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(BaseSecureURL2) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 404) // with client certs, creds shouldn't matter resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2 + "/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) resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool}) defer func() { resty.SetTLSClientConfig(nil) }() config := api.NewConfig() config.HTTP.Port = SecurePort2 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(BaseURL2) 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(BaseURL2) So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 400) // without client certs and creds, should fail _, err = resty.R().Get(BaseSecureURL2) 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(BaseSecureURL2) 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(BaseSecureURL2 + "/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(BaseSecureURL2) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 404) resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2 + "/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) resty.SetTLSClientConfig(&tls.Config{RootCAs: caCertPool}) defer func() { resty.SetTLSClientConfig(nil) }() config := api.NewConfig() config.HTTP.Port = SecurePort2 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(BaseURL2) 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(BaseURL2) So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 400) // without client certs and creds, should fail _, err = resty.R().Get(BaseSecureURL2) 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(BaseSecureURL2) 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(BaseSecureURL2 + "/v2/") So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) // with only client certs, writes should fail resp, err = resty.R().Post(BaseSecureURL2 + "/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(BaseSecureURL2) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 404) resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseSecureURL2 + "/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.New("ldap: bind creds required") } if (bindDN == LDAPBindDN && bindSimplePw == LDAPBindPassword) || (bindDN == fmt.Sprintf("cn=%s,%s", username, LDAPBaseDN) && bindSimplePw == passphrase) { return vldap.LDAPResultSuccess, nil } return vldap.LDAPResultInvalidCredentials, errors.New("ldap: invalid credentials") } 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() config := api.NewConfig() config.HTTP.Port = SecurePort1 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(BaseURL1) 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(BaseURL1 + "/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(BaseURL1) So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 404) resp, _ = resty.R().SetBasicAuth(username, passphrase).Get(BaseURL1 + "/v2/") So(resp, ShouldNotBeNil) So(resp.StatusCode(), ShouldEqual, 200) }) }