diff --git a/examples/config-ldap-credentials.json b/examples/config-ldap-credentials.json new file mode 100644 index 0000000000..4b81c5f983 --- /dev/null +++ b/examples/config-ldap-credentials.json @@ -0,0 +1,4 @@ +{ + "bindDN":"cn=ldap-searcher,ou=Users,dc=example,dc=org", + "bindPassword":"ldap-searcher-password" +} \ No newline at end of file diff --git a/examples/config-ldap.json b/examples/config-ldap.json index 4656e6fd5b..400ab88de9 100644 --- a/examples/config-ldap.json +++ b/examples/config-ldap.json @@ -13,14 +13,13 @@ }, "auth": { "ldap": { + "credentialsFile": "examples/config-ldap-credentials.json", "address": "ldap.example.org", "port": 389, "startTLS": false, "baseDN":"ou=Users,dc=example,dc=org", "userAttribute": "uid", "userGroupAttribute": "memberOf", - "bindDN":"cn=ldap-searcher,ou=Users,dc=example,dc=org", - "bindPassword":"ldap-searcher-password", "skipVerify": true, "subtreeSearch": true }, diff --git a/pkg/api/authn.go b/pkg/api/authn.go index 9777b8bee9..741777dd08 100644 --- a/pkg/api/authn.go +++ b/pkg/api/authn.go @@ -266,9 +266,9 @@ func (amw *AuthnMiddleware) tryAuthnHandlers(ctlr *Controller) mux.MiddlewareFun UseSSL: !ldapConfig.Insecure, SkipTLS: !ldapConfig.StartTLS, Base: ldapConfig.BaseDN, - BindDN: ldapConfig.BindDN, + BindDN: ldapConfig.BindDN(), + BindPassword: ldapConfig.BindPassword(), UserGroupAttribute: ldapConfig.UserGroupAttribute, // from config - BindPassword: ldapConfig.BindPassword, UserFilter: fmt.Sprintf("(%s=%%s)", ldapConfig.UserAttribute), InsecureSkipVerify: ldapConfig.SkipVerify, ServerName: ldapConfig.Address, diff --git a/pkg/api/config/config.go b/pkg/api/config/config.go index 5dbcf343a5..5d0f68dbf6 100644 --- a/pkg/api/config/config.go +++ b/pkg/api/config/config.go @@ -121,21 +121,47 @@ type SchedulerConfig struct { NumWorkers int } +type LDAPCredentials struct { + BindDN string + BindPassword string +} + type LDAPConfig struct { + CredentialsFile string Port int Insecure bool StartTLS bool // if !Insecure, then StartTLS or LDAPs SkipVerify bool SubtreeSearch bool Address string - BindDN string + bindDN string `json:"-"` + bindPassword string `json:"-"` UserGroupAttribute string - BindPassword string BaseDN string UserAttribute string CACert string } +func (ldapConf *LDAPConfig) BindDN() string { + return ldapConf.bindDN +} + +func (ldapConf *LDAPConfig) SetBindDN(bindDN string) *LDAPConfig { + ldapConf.bindDN = bindDN + + return ldapConf +} + +func (ldapConf *LDAPConfig) BindPassword() string { + return ldapConf.bindPassword +} + +func (ldapConf *LDAPConfig) SetBindPassword(bindPassword string) *LDAPConfig { + ldapConf.bindPassword = bindPassword + + return ldapConf +} + type LogConfig struct { Level string Output string @@ -266,14 +292,14 @@ func (c *Config) Sanitize() *Config { panic(err) } - if c.HTTP.Auth != nil && c.HTTP.Auth.LDAP != nil && c.HTTP.Auth.LDAP.BindPassword != "" { + if c.HTTP.Auth != nil && c.HTTP.Auth.LDAP != nil && c.HTTP.Auth.LDAP.bindPassword != "" { sanitizedConfig.HTTP.Auth.LDAP = &LDAPConfig{} if err := DeepCopy(c.HTTP.Auth.LDAP, sanitizedConfig.HTTP.Auth.LDAP); err != nil { panic(err) } - sanitizedConfig.HTTP.Auth.LDAP.BindPassword = "******" + sanitizedConfig.HTTP.Auth.LDAP.bindPassword = "******" } return sanitizedConfig diff --git a/pkg/api/config/config_test.go b/pkg/api/config/config_test.go index 72c5e0a858..cbf3aeb029 100644 --- a/pkg/api/config/config_test.go +++ b/pkg/api/config/config_test.go @@ -69,11 +69,11 @@ func TestConfig(t *testing.T) { Convey("Test DeepCopy() & Sanitize()", t, func() { conf := config.New() So(conf, ShouldNotBeNil) - authConfig := &config.AuthConfig{LDAP: &config.LDAPConfig{BindPassword: "oina"}} + authConfig := &config.AuthConfig{LDAP: (&config.LDAPConfig{}).SetBindPassword("oina")} conf.HTTP.Auth = authConfig So(func() { conf.Sanitize() }, ShouldNotPanic) conf = conf.Sanitize() - So(conf.HTTP.Auth.LDAP.BindPassword, ShouldEqual, "******") + So(conf.HTTP.Auth.LDAP.BindPassword(), ShouldEqual, "******") // negative obj := make(chan int) diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index b2ce80ae83..9e245168d4 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -53,6 +53,7 @@ import ( "zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/constants" apiErr "zotregistry.io/zot/pkg/api/errors" + "zotregistry.io/zot/pkg/cli/server" "zotregistry.io/zot/pkg/common" extconf "zotregistry.io/zot/pkg/extensions/config" "zotregistry.io/zot/pkg/log" @@ -1985,8 +1986,13 @@ func (l *testLDAPServer) Stop() { } func (l *testLDAPServer) Bind(bindDN, bindSimplePw string, conn net.Conn) (vldap.LDAPResultCode, error) { - if bindDN == "" || bindSimplePw == "" { - return vldap.LDAPResultInappropriateAuthentication, errors.ErrRequireCred + if bindSimplePw == "" { + switch bindDN { + case "bad-user", "cn=fail-user-bind,ou=test": + return vldap.LDAPResultInvalidCredentials, errors.ErrInvalidCred + default: + return vldap.LDAPResultSuccess, nil + } } if (bindDN == LDAPBindDN && bindSimplePw == LDAPBindPassword) || @@ -2000,7 +2006,25 @@ func (l *testLDAPServer) Bind(bindDN, bindSimplePw string, conn net.Conn) (vldap func (l *testLDAPServer) Search(boundDN string, req vldap.SearchRequest, conn net.Conn, ) (vldap.ServerSearchResult, error) { + if req.Filter == "(uid=fail-user-bind)" { + return vldap.ServerSearchResult{ + Entries: []*vldap.Entry{ + { + DN: fmt.Sprintf("cn=%s,%s", "fail-user-bind", LDAPBaseDN), + Attributes: []*vldap.EntryAttribute{ + { + Name: "memberOf", + Values: []string{group}, + }, + }, + }, + }, + ResultCode: vldap.LDAPResultSuccess, + }, nil + } + check := fmt.Sprintf("(uid=%s)", username) + if check == req.Filter { return vldap.ServerSearchResult{ Entries: []*vldap.Entry{ @@ -2036,15 +2060,13 @@ func TestBasicAuthWithLDAP(t *testing.T) { conf := config.New() conf.HTTP.Port = port conf.HTTP.Auth = &config.AuthConfig{ - LDAP: &config.LDAPConfig{ + LDAP: (&config.LDAPConfig{ Insecure: true, Address: LDAPAddress, Port: ldapPort, - BindDN: LDAPBindDN, - BindPassword: LDAPBindPassword, BaseDN: LDAPBaseDN, UserAttribute: "uid", - }, + }).SetBindDN(LDAPBindDN).SetBindPassword(LDAPBindPassword), } ctlr := makeController(conf, t.TempDir()) @@ -2077,6 +2099,293 @@ func TestBasicAuthWithLDAP(t *testing.T) { }) } +func TestLDAPWithoutCreds(t *testing.T) { + Convey("Make a new LDAP server", t, func() { + l := newTestLDAPServer() + port := test.GetFreePort() + ldapPort, err := strconv.Atoi(port) + So(err, ShouldBeNil) + l.Start(ldapPort) + defer l.Stop() + + Convey("Server credentials succed ldap auth", func() { + port = test.GetFreePort() + baseURL := test.GetBaseURL(port) + + conf := config.New() + conf.HTTP.Port = port + conf.HTTP.Auth = &config.AuthConfig{ + LDAP: (&config.LDAPConfig{ + Insecure: true, + Address: LDAPAddress, + Port: ldapPort, + BaseDN: LDAPBaseDN, + UserAttribute: "uid", + }).SetBindDN("anonym"), + } + ctlr := makeController(conf, t.TempDir()) + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() + + // without creds, should get access error + resp, err := resty.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) + + resp, _ = resty.R().SetBasicAuth(username, "").Get(baseURL) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) + + resp, _ = resty.R().SetBasicAuth(username, "").Get(baseURL + "/v2/") + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + + resp, _ = resty.R().SetBasicAuth(username, password).Get(baseURL + "/v2/") + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusOK) + }) + + Convey("Server credentials fail ldap auth", func() { + port = test.GetFreePort() + baseURL := test.GetBaseURL(port) + + conf := config.New() + conf.HTTP.Port = port + conf.HTTP.Auth = &config.AuthConfig{ + LDAP: (&config.LDAPConfig{ + Insecure: true, + Address: LDAPAddress, + Port: ldapPort, + BaseDN: LDAPBaseDN, + UserAttribute: "uid", + }).SetBindDN("bad-user"), + } + ctlr := makeController(conf, t.TempDir()) + + cm := test.NewControllerManager(ctlr) + cm.StartAndWait(port) + defer cm.StopServer() + + resp, _ := resty.R().SetBasicAuth(username, password).Get(baseURL + "/v2/") + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusUnauthorized) + }) + }) +} + +func TestBasicAuthWithLDAPFromFile(t *testing.T) { + Convey("Make a new controller", t, func() { + l := newTestLDAPServer() + port := test.GetFreePort() + ldapPort, err := strconv.Atoi(port) + So(err, ShouldBeNil) + l.Start(ldapPort) + defer l.Stop() + + port = test.GetFreePort() + baseURL := test.GetBaseURL(port) + tempDir := t.TempDir() + + ldapConfigContent := fmt.Sprintf(` + { + "BindDN": "%v", + "BindPassword": "%v" + }`, LDAPBindDN, LDAPBindPassword) + + ldapConfigPath := filepath.Join(tempDir, "ldap.json") + + err = os.WriteFile(ldapConfigPath, []byte(ldapConfigContent), 0o600) + So(err, ShouldBeNil) + + configStr := fmt.Sprintf(` + { + "Storage": { + "RootDirectory": "%s" + }, + "HTTP": { + "Address": "%s", + "Port": "%s", + "Auth": { + "LDAP": { + "CredentialsFile": "%s", + "BaseDN": "%v", + "UserAttribute": "uid", + "UserGroupAttribute": "memberOf", + "Insecure": true, + "Address": "%v", + "Port": %v + } + } + } + }`, tempDir, "127.0.0.1", port, ldapConfigPath, LDAPBaseDN, LDAPAddress, ldapPort) + + configPath := filepath.Join(tempDir, "config.json") + + err = os.WriteFile(configPath, []byte(configStr), 0o0600) + So(err, ShouldBeNil) + + server := server.NewServerRootCmd() + server.SetArgs([]string{"serve", configPath}) + go func() { + err := server.Execute() + if err != nil { + panic(err) + } + }() + + test.WaitTillServerReady(baseURL) + + // without creds, should get access error + resp, err := resty.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) + + // with creds, should get expected status code + resp, _ = resty.R().SetBasicAuth(username, password).Get(baseURL) + So(resp, ShouldNotBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusNotFound) + + resp, _ = resty.R().SetBasicAuth(username, password).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 TestLDAPConfigErrors(t *testing.T) { + const configTemplate = ` + { + "Storage": { + "RootDirectory": "%s" + }, + "HTTP": { + "Address": "%s", + "Port": "%s", + "Auth": { + "LDAP": { + "CredentialsFile": "%s", + "BaseDN": "%v", + "UserAttribute": "%v", + "UserGroupAttribute": "memberOf", + "Insecure": true, + "Address": "%v", + "Port": %v + } + } + } + }` + + Convey("bad credentials file", t, func() { + conf := config.New() + tempDir := t.TempDir() + ldapPort := 9000 + userAttribute := "" + + ldapConfigContent := `bad-json` + ldapConfigPath := filepath.Join(tempDir, "ldap.json") + err := os.WriteFile(ldapConfigPath, []byte(ldapConfigContent), 0o600) + So(err, ShouldBeNil) + + configStr := fmt.Sprintf(configTemplate, + tempDir, "127.0.0.1", "8000", ldapConfigPath, LDAPBaseDN, userAttribute, LDAPAddress, ldapPort) + configPath := filepath.Join(tempDir, "config.json") + err = os.WriteFile(configPath, []byte(configStr), 0o0600) + So(err, ShouldBeNil) + + err = server.LoadConfiguration(conf, configPath) + So(err, ShouldNotBeNil) + }) + + Convey("UserAttribute is empty", t, func() { + conf := config.New() + tempDir := t.TempDir() + ldapPort := 9000 + userAttribute := "" + + ldapConfigContent := fmt.Sprintf(` + { + "BindDN": "%v", + "BindPassword": "%v" + }`, LDAPBindDN, LDAPBindPassword) + ldapConfigPath := filepath.Join(tempDir, "ldap.json") + err := os.WriteFile(ldapConfigPath, []byte(ldapConfigContent), 0o600) + So(err, ShouldBeNil) + + configStr := fmt.Sprintf(configTemplate, + tempDir, "127.0.0.1", "8000", ldapConfigPath, LDAPBaseDN, userAttribute, LDAPAddress, ldapPort) + configPath := filepath.Join(tempDir, "config.json") + err = os.WriteFile(configPath, []byte(configStr), 0o0600) + So(err, ShouldBeNil) + + err = server.LoadConfiguration(conf, configPath) + So(err, ShouldNotBeNil) + }) + + Convey("address is empty", t, func() { + conf := config.New() + tempDir := t.TempDir() + ldapPort := 9000 + userAttribute := "uid" + + ldapConfigContent := fmt.Sprintf(` + { + "BindDN": "%v", + "BindPassword": "%v" + }`, LDAPBindDN, LDAPBindPassword) + ldapConfigPath := filepath.Join(tempDir, "ldap.json") + err := os.WriteFile(ldapConfigPath, []byte(ldapConfigContent), 0o600) + So(err, ShouldBeNil) + + configStr := fmt.Sprintf(configTemplate, + tempDir, "127.0.0.1", "8000", ldapConfigPath, LDAPBaseDN, userAttribute, "", ldapPort) + configPath := filepath.Join(tempDir, "config.json") + err = os.WriteFile(configPath, []byte(configStr), 0o0600) + So(err, ShouldBeNil) + + err = server.LoadConfiguration(conf, configPath) + So(err, ShouldNotBeNil) + }) + + Convey("BaseDN is empty", t, func() { + conf := config.New() + tempDir := t.TempDir() + ldapPort := 9000 + userAttribute := "uid" + + ldapConfigContent := fmt.Sprintf(` + { + "BindDN": "%v", + "BindPassword": "%v" + }`, LDAPBindDN, LDAPBindPassword) + ldapConfigPath := filepath.Join(tempDir, "ldap.json") + err := os.WriteFile(ldapConfigPath, []byte(ldapConfigContent), 0o600) + So(err, ShouldBeNil) + + configStr := fmt.Sprintf(configTemplate, + tempDir, "127.0.0.1", "8000", ldapConfigPath, "", userAttribute, LDAPAddress, ldapPort) + configPath := filepath.Join(tempDir, "config.json") + err = os.WriteFile(configPath, []byte(configStr), 0o0600) + So(err, ShouldBeNil) + + err = server.LoadConfiguration(conf, configPath) + So(err, ShouldNotBeNil) + }) +} + func TestGroupsPermissionsForLDAP(t *testing.T) { Convey("Make a new controller", t, func() { l := newTestLDAPServer() @@ -2093,16 +2402,14 @@ func TestGroupsPermissionsForLDAP(t *testing.T) { conf := config.New() conf.HTTP.Port = port conf.HTTP.Auth = &config.AuthConfig{ - LDAP: &config.LDAPConfig{ + LDAP: (&config.LDAPConfig{ Insecure: true, Address: LDAPAddress, Port: ldapPort, - BindDN: LDAPBindDN, - BindPassword: LDAPBindPassword, BaseDN: LDAPBaseDN, UserAttribute: "uid", UserGroupAttribute: "memberOf", - }, + }).SetBindDN(LDAPBindDN).SetBindPassword(LDAPBindPassword), } repoName, seed := test.GenerateRandomName() @@ -2145,6 +2452,101 @@ func TestGroupsPermissionsForLDAP(t *testing.T) { }) } +func TestLDAPConfigFromFile(t *testing.T) { + Convey("Make a new controller", t, func() { + l := newTestLDAPServer() + port := test.GetFreePort() + ldapPort, err := strconv.Atoi(port) + So(err, ShouldBeNil) + l.Start(ldapPort) + defer l.Stop() + + port = test.GetFreePort() + baseURL := test.GetBaseURL(port) + tempDir := t.TempDir() + + ldapConfigContent := fmt.Sprintf(` + { + "bindDN": "%v", + "bindPassword": "%v" + }`, LDAPBindDN, LDAPBindPassword) + + ldapConfigPath := filepath.Join(tempDir, "ldap.json") + + err = os.WriteFile(ldapConfigPath, []byte(ldapConfigContent), 0o600) + So(err, ShouldBeNil) + + configStr := fmt.Sprintf(` + { + "Storage": { + "RootDirectory": "%s" + }, + "HTTP": { + "Address": "%s", + "Port": "%s", + "Auth": { + "LDAP": { + "CredentialsFile": "%s", + "BaseDN": "%v", + "UserAttribute": "uid", + "UserGroupAttribute": "memberOf", + "Insecure": true, + "Address": "%v", + "Port": %v + } + }, + "AccessControl": { + "repositories": { + "test-ldap": { + "Policies": [ + { + "Users": null, + "Actions": [ + "read", + "create" + ], + "Groups": [ + "test" + ] + } + ] + } + }, + "Groups": { + "test": { + "Users": [ + "test" + ] + } + } + } + } + }`, tempDir, "127.0.0.1", port, ldapConfigPath, LDAPBaseDN, LDAPAddress, ldapPort) + + configPath := filepath.Join(tempDir, "config.json") + + err = os.WriteFile(configPath, []byte(configStr), 0o0600) + So(err, ShouldBeNil) + + server := server.NewServerRootCmd() + server.SetArgs([]string{"serve", configPath}) + go func() { + err := server.Execute() + if err != nil { + panic(err) + } + }() + + test.WaitTillServerReady(baseURL) + + repo := "test-ldap" + img := CreateDefaultImage() + + err = UploadImageWithBasicAuth(img, baseURL, repo, img.DigestStr(), username, password) + So(err, ShouldBeNil) + }) +} + func TestLDAPFailures(t *testing.T) { Convey("Make a LDAP conn", t, func() { l := newTestLDAPServer() @@ -2181,6 +2583,69 @@ func TestLDAPFailures(t *testing.T) { }) } +func TestLDAPClient(t *testing.T) { + Convey("LDAP Client", t, func() { + l := newTestLDAPServer() + port := test.GetFreePort() + ldapPort, err := strconv.Atoi(port) + So(err, ShouldBeNil) + l.Start(ldapPort) + defer l.Stop() + + // bad server credentials + lClient := &api.LDAPClient{ + Host: LDAPAddress, + Port: ldapPort, + BindDN: "bad-user", + BindPassword: "bad-pass", + SkipTLS: true, + } + + _, _, _, err = lClient.Authenticate("bad-user", "bad-pass") + So(err, ShouldNotBeNil) + + // bad credentials with anonymous authentication + lClient = &api.LDAPClient{ + Host: LDAPAddress, + Port: ldapPort, + BindDN: "bad-user", + BindPassword: "", + SkipTLS: true, + } + + _, _, _, err = lClient.Authenticate("user", "") + So(err, ShouldNotBeNil) + + // bad user credentials with anonymous authentication + lClient = &api.LDAPClient{ + Host: LDAPAddress, + Port: ldapPort, + BindDN: LDAPBindDN, + BindPassword: LDAPBindPassword, + Base: LDAPBaseDN, + UserFilter: "(uid=%s)", + SkipTLS: true, + } + + _, _, _, err = lClient.Authenticate("fail-user-bind", "") + So(err, ShouldNotBeNil) + + // bad user credentials with anonymous authentication + lClient = &api.LDAPClient{ + Host: LDAPAddress, + Port: ldapPort, + BindDN: LDAPBindDN, + BindPassword: LDAPBindPassword, + Base: LDAPBaseDN, + UserFilter: "(uid=%s)", + SkipTLS: true, + } + + _, _, _, err = lClient.Authenticate("fail-user-bind", "pass") + So(err, ShouldNotBeNil) + }) +} + func TestBearerAuth(t *testing.T) { Convey("Make a new controller", t, func() { authTestServer := authutils.MakeAuthTestServer(ServerKey, UnauthorizedNamespace) @@ -2681,15 +3146,13 @@ func TestOpenIDMiddleware(t *testing.T) { HTPasswd: config.AuthHTPasswd{ Path: htpasswdPath, }, - LDAP: &config.LDAPConfig{ + LDAP: (&config.LDAPConfig{ Insecure: true, Address: LDAPAddress, Port: ldapPort, - BindDN: LDAPBindDN, - BindPassword: LDAPBindPassword, BaseDN: LDAPBaseDN, UserAttribute: "uid", - }, + }).SetBindDN(LDAPBindDN).SetBindPassword(LDAPBindPassword), OpenID: &config.OpenIDConfig{ Providers: map[string]config.OpenIDProviderConfig{ "oidc": { @@ -3162,15 +3625,13 @@ func TestAuthnSessionErrors(t *testing.T) { HTPasswd: config.AuthHTPasswd{ Path: htpasswdPath, }, - LDAP: &config.LDAPConfig{ + LDAP: (&config.LDAPConfig{ Insecure: true, Address: LDAPAddress, Port: ldapPort, - BindDN: LDAPBindDN, - BindPassword: LDAPBindPassword, BaseDN: LDAPBaseDN, UserAttribute: "uid", - }, + }).SetBindDN(LDAPBindDN).SetBindPassword(LDAPBindPassword), OpenID: &config.OpenIDConfig{ Providers: map[string]config.OpenIDProviderConfig{ "oidc": { diff --git a/pkg/api/ldap.go b/pkg/api/ldap.go index a5695dda7c..c04a917770 100644 --- a/pkg/api/ldap.go +++ b/pkg/api/ldap.go @@ -105,7 +105,7 @@ func (lc *LDAPClient) Close() { } } -const maxRetries = 8 +const maxRetries = 1 func sleepAndRetry(retries, maxRetries int) bool { if retries > maxRetries { @@ -140,7 +140,7 @@ func (lc *LDAPClient) Authenticate(username, password string) (bool, map[string] } // First bind with a read only user - if lc.BindDN != "" && lc.BindPassword != "" { + if lc.BindPassword != "" { err := lc.Conn.Bind(lc.BindDN, lc.BindPassword) if err != nil { lc.Log.Error().Err(err).Str("bindDN", lc.BindDN).Msg("bind failed") @@ -148,6 +148,16 @@ func (lc *LDAPClient) Authenticate(username, password string) (bool, map[string] lc.Conn.Close() lc.Conn = nil + continue + } + } else { + err := lc.Conn.UnauthenticatedBind(lc.BindDN) + if err != nil { + lc.Log.Error().Err(err).Str("bindDN", lc.BindDN).Msg("bind failed") + // clean up the cached conn, so we can retry + lc.Conn.Close() + lc.Conn = nil + continue } } diff --git a/pkg/cli/server/root.go b/pkg/cli/server/root.go index d49f3902aa..ab0c1d3bdb 100644 --- a/pkg/cli/server/root.go +++ b/pkg/cli/server/root.go @@ -738,6 +738,10 @@ func LoadConfiguration(config *config.Config, configPath string) error { return zerr.ErrBadConfig } + if err := updateLDAPConfig(config); err != nil { + return err + } + // defaults applyDefaultValues(config, viperInstance, log) @@ -752,6 +756,50 @@ func LoadConfiguration(config *config.Config, configPath string) error { return nil } +func updateLDAPConfig(conf *config.Config) error { + if conf.HTTP.Auth == nil || conf.HTTP.Auth.LDAP == nil { + return nil + } + + if conf.HTTP.Auth.LDAP.CredentialsFile == "" { + conf.HTTP.Auth.LDAP.SetBindDN("anonym-user") + + return nil + } + + newLDAPCredentials, err := readLDAPCredentials(conf.HTTP.Auth.LDAP.CredentialsFile) + if err != nil { + return err + } + + conf.HTTP.Auth.LDAP.SetBindDN(newLDAPCredentials.BindDN) + conf.HTTP.Auth.LDAP.SetBindPassword(newLDAPCredentials.BindPassword) + + return nil +} + +func readLDAPCredentials(ldapConfigPath string) (config.LDAPCredentials, error) { + viperInstance := viper.NewWithOptions(viper.KeyDelimiter("::")) + + viperInstance.SetConfigFile(ldapConfigPath) + + if err := viperInstance.ReadInConfig(); err != nil { + log.Error().Err(err).Msg("error while reading configuration") + + return config.LDAPCredentials{}, err + } + + var ldapCredentials config.LDAPCredentials + + if err := viperInstance.Unmarshal(&ldapCredentials); err != nil { + log.Error().Err(err).Msg("error while unmarshaling new config") + + return config.LDAPCredentials{}, err + } + + return ldapCredentials, nil +} + func authzContainsOnlyAnonymousPolicy(cfg *config.Config) bool { adminPolicy := cfg.HTTP.AccessControl.AdminPolicy anonymousPolicyPresent := false diff --git a/pkg/cli/server/root_test.go b/pkg/cli/server/root_test.go index aa9dd1a5d6..a7b5e13078 100644 --- a/pkg/cli/server/root_test.go +++ b/pkg/cli/server/root_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path" + "path/filepath" "testing" "time" @@ -1447,6 +1448,88 @@ func TestScrub(t *testing.T) { }) } +func TestUpdateLDAPConfig(t *testing.T) { + Convey("updateLDAPConfig errors while unmarshaling ldap config", t, func() { + tempDir := t.TempDir() + ldapConfigContent := "bad-json" + ldapConfigPath := filepath.Join(tempDir, "ldap.json") + + err := os.WriteFile(ldapConfigPath, []byte(ldapConfigContent), 0o000) + So(err, ShouldBeNil) + + configStr := fmt.Sprintf(` + { + "Storage": { + "RootDirectory": "%s" + }, + "HTTP": { + "Address": "%s", + "Port": "%s", + "Auth": { + "LDAP": { + "CredentialsFile": "%s", + "BaseDN": "%v", + "UserAttribute": "uid", + "UserGroupAttribute": "memberOf", + "Insecure": true, + "Address": "%v", + "Port": %v + } + } + } + }`, tempDir, "127.0.0.1", "8000", ldapConfigPath, "LDAPBaseDN", "LDAPAddress", 1000) + + configPath := filepath.Join(tempDir, "config.json") + + err = os.WriteFile(configPath, []byte(configStr), 0o0600) + So(err, ShouldBeNil) + + server := cli.NewServerRootCmd() + server.SetArgs([]string{"serve", configPath}) + So(func() { err = server.Execute() }, ShouldPanic) + + err = os.Chmod(ldapConfigPath, 0o600) + So(err, ShouldBeNil) + + server = cli.NewServerRootCmd() + server.SetArgs([]string{"serve", configPath}) + So(func() { err = server.Execute() }, ShouldPanic) + }) + + Convey("unauthenticated LDAP config", t, func() { + tempDir := t.TempDir() + + configStr := fmt.Sprintf(` + { + "Storage": { + "RootDirectory": "%s" + }, + "HTTP": { + "Address": "%s", + "Port": "%s", + "Auth": { + "LDAP": { + "BaseDN": "%v", + "UserAttribute": "uid", + "UserGroupAttribute": "memberOf", + "Insecure": true, + "Address": "%v", + "Port": %v + } + } + } + }`, tempDir, "127.0.0.1", "8000", "LDAPBaseDN", "LDAPAddress", 1000) + + configPath := filepath.Join(tempDir, "config.json") + + err := os.WriteFile(configPath, []byte(configStr), 0o0600) + So(err, ShouldBeNil) + + err = cli.LoadConfiguration(config.New(), configPath) + So(err, ShouldBeNil) + }) +} + // run cli and return output. func runCLIWithConfig(tempDir string, config string) (string, error) { port := GetFreePort() diff --git a/pkg/extensions/extensions_test.go b/pkg/extensions/extensions_test.go index 002825cc16..593bccfc2d 100644 --- a/pkg/extensions/extensions_test.go +++ b/pkg/extensions/extensions_test.go @@ -225,11 +225,10 @@ func TestMgmtExtension(t *testing.T) { Convey("Verify mgmt auth info route enabled with ldap", t, func() { defer os.Remove(conf.HTTP.Auth.HTPasswd.Path) // cleanup of a file created in previous Convey - conf.HTTP.Auth.LDAP = &config.LDAPConfig{ - BindDN: "binddn", + conf.HTTP.Auth.LDAP = (&config.LDAPConfig{ BaseDN: "basedn", Address: "ldapexample", - } + }).SetBindDN("binddn") conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions.Search = &extconf.SearchConfig{} @@ -290,11 +289,10 @@ func TestMgmtExtension(t *testing.T) { htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) defer os.Remove(htpasswdPath) conf.HTTP.Auth.HTPasswd.Path = htpasswdPath - conf.HTTP.Auth.LDAP = &config.LDAPConfig{ - BindDN: "binddn", + conf.HTTP.Auth.LDAP = (&config.LDAPConfig{ BaseDN: "basedn", Address: "ldapexample", - } + }).SetBindDN("binddn") conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions.Search = &extconf.SearchConfig{} @@ -369,11 +367,10 @@ func TestMgmtExtension(t *testing.T) { htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password)) defer os.Remove(htpasswdPath) conf.HTTP.Auth.HTPasswd.Path = htpasswdPath - conf.HTTP.Auth.LDAP = &config.LDAPConfig{ - BindDN: "binddn", + conf.HTTP.Auth.LDAP = (&config.LDAPConfig{ BaseDN: "basedn", Address: "ldapexample", - } + }).SetBindDN("binddn") conf.HTTP.Auth.Bearer = &config.BearerConfig{ Realm: "realm", @@ -449,11 +446,10 @@ func TestMgmtExtension(t *testing.T) { Convey("Verify mgmt auth info route enabled with ldap + bearer", t, func() { conf.HTTP.Auth.HTPasswd.Path = "" - conf.HTTP.Auth.LDAP = &config.LDAPConfig{ - BindDN: "binddn", + conf.HTTP.Auth.LDAP = (&config.LDAPConfig{ BaseDN: "basedn", Address: "ldapexample", - } + }).SetBindDN("binddn") conf.HTTP.Auth.Bearer = &config.BearerConfig{ Realm: "realm",