feat(ldap): add option to load ldap from file (#1778)

Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
This commit is contained in:
LaurentiuNiculae
2023-11-15 02:21:36 +02:00
committed by GitHub
parent b2a9239c03
commit 272eb7cc43
10 changed files with 668 additions and 41 deletions
+2 -2
View File
@@ -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,
+30 -4
View File
@@ -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
+2 -2
View File
@@ -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)
+479 -18
View File
@@ -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": {
+11 -1
View File
@@ -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
}
}