Skip to content

Commit 1785688

Browse files
feat(ldap): hot reloading ldap credentials on change (#2167)
Signed-off-by: Laurentiu Niculae <[email protected]>
1 parent 8215766 commit 1785688

File tree

6 files changed

+236
-15
lines changed

6 files changed

+236
-15
lines changed

pkg/api/authn.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,8 @@ func (amw *AuthnMiddleware) tryAuthnHandlers(ctlr *Controller) mux.MiddlewareFun
262262
// ldap and htpasswd based authN
263263
if ctlr.Config.IsLdapAuthEnabled() {
264264
ldapConfig := ctlr.Config.HTTP.Auth.LDAP
265-
amw.ldapClient = &LDAPClient{
265+
266+
ctlr.LDAPClient = &LDAPClient{
266267
Host: ldapConfig.Address,
267268
Port: ldapConfig.Port,
268269
UseSSL: !ldapConfig.Insecure,
@@ -278,6 +279,8 @@ func (amw *AuthnMiddleware) tryAuthnHandlers(ctlr *Controller) mux.MiddlewareFun
278279
SubtreeSearch: ldapConfig.SubtreeSearch,
279280
}
280281

282+
amw.ldapClient = ctlr.LDAPClient
283+
281284
if ctlr.Config.HTTP.Auth.LDAP.CACert != "" {
282285
caCert, err := os.ReadFile(ctlr.Config.HTTP.Auth.LDAP.CACert)
283286
if err != nil {

pkg/api/controller.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type Controller struct {
4848
SyncOnDemand SyncOnDemand
4949
RelyingParties map[string]rp.RelyingParty
5050
CookieStore *CookieStore
51+
LDAPClient *LDAPClient
5152
taskScheduler *scheduler.Scheduler
5253
// runtime params
5354
chosenPort int // kernel-chosen port
@@ -313,6 +314,17 @@ func (c *Controller) LoadNewConfig(newConfig *config.Config) {
313314
// reload access control config
314315
c.Config.HTTP.AccessControl = newConfig.HTTP.AccessControl
315316

317+
if c.Config.HTTP.Auth != nil {
318+
c.Config.HTTP.Auth.LDAP = newConfig.HTTP.Auth.LDAP
319+
320+
if c.LDAPClient != nil {
321+
c.LDAPClient.lock.Lock()
322+
c.LDAPClient.BindDN = newConfig.HTTP.Auth.LDAP.BindDN()
323+
c.LDAPClient.BindPassword = newConfig.HTTP.Auth.LDAP.BindPassword()
324+
c.LDAPClient.lock.Unlock()
325+
}
326+
}
327+
316328
// reload periodical gc config
317329
c.Config.Storage.GC = newConfig.Storage.GC
318330
c.Config.Storage.Dedupe = newConfig.Storage.Dedupe

pkg/api/controller_test.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2331,6 +2331,183 @@ func TestBasicAuthWithLDAP(t *testing.T) {
23312331
})
23322332
}
23332333

2334+
func TestBasicAuthWithReloadedCredentials(t *testing.T) {
2335+
Convey("Start server with bad credentials", t, func() {
2336+
l := newTestLDAPServer()
2337+
ldapPort, err := strconv.Atoi(test.GetFreePort())
2338+
So(err, ShouldBeNil)
2339+
l.Start(ldapPort)
2340+
defer l.Stop()
2341+
2342+
tempDir := t.TempDir()
2343+
ldapConfigContent := fmt.Sprintf(`{"BindDN": "%s", "BindPassword": "%s"}`, LDAPBindDN, LDAPBindPassword)
2344+
ldapConfigPath := filepath.Join(tempDir, "ldap.json")
2345+
2346+
err = os.WriteFile(ldapConfigPath, []byte(ldapConfigContent), 0o600)
2347+
So(err, ShouldBeNil)
2348+
2349+
port := test.GetFreePort()
2350+
baseURL := test.GetBaseURL(port)
2351+
2352+
configTemplate := `
2353+
{
2354+
"Storage": {
2355+
"rootDirectory": "%s"
2356+
},
2357+
"HTTP": {
2358+
"Address": "%s",
2359+
"Port": "%s",
2360+
"Auth": {
2361+
"LDAP": {
2362+
"CredentialsFile": "%s",
2363+
"UserAttribute": "uid",
2364+
"BaseDN": "LDAPBaseDN",
2365+
"UserGroupAttribute": "memberOf",
2366+
"Insecure": true,
2367+
"Address": "%v",
2368+
"Port": %v
2369+
}
2370+
}
2371+
}
2372+
}
2373+
`
2374+
2375+
configStr := fmt.Sprintf(configTemplate,
2376+
tempDir, "127.0.0.1", port, ldapConfigPath, LDAPAddress, ldapPort)
2377+
2378+
configPath := filepath.Join(tempDir, "config.json")
2379+
err = os.WriteFile(configPath, []byte(configStr), 0o600)
2380+
So(err, ShouldBeNil)
2381+
2382+
conf := config.New()
2383+
err = server.LoadConfiguration(conf, configPath)
2384+
So(err, ShouldBeNil)
2385+
2386+
ctlr := api.NewController(conf)
2387+
ctlrManager := test.NewControllerManager(ctlr)
2388+
2389+
hotReloader, err := server.NewHotReloader(ctlr, configPath, ldapConfigPath)
2390+
So(err, ShouldBeNil)
2391+
2392+
hotReloader.Start()
2393+
2394+
ctlrManager.StartAndWait(port)
2395+
defer ctlrManager.StopServer()
2396+
time.Sleep(time.Second * 2)
2397+
2398+
// test if the credentials work
2399+
resp, _ := resty.R().SetBasicAuth(username, password).Get(baseURL + "/v2/")
2400+
So(resp, ShouldNotBeNil)
2401+
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
2402+
2403+
newLdapConfigContent := `{"BindDN": "NewBindDB", "BindPassword": "NewBindPassword"}`
2404+
2405+
err = os.WriteFile(ldapConfigPath, []byte(newLdapConfigContent), 0o600)
2406+
So(err, ShouldBeNil)
2407+
2408+
for i := 0; i < 10; i++ {
2409+
// test if the credentials don't work
2410+
resp, _ = resty.R().SetBasicAuth(username, password).Get(baseURL + "/v2/")
2411+
So(resp, ShouldNotBeNil)
2412+
2413+
if resp.StatusCode() != http.StatusOK {
2414+
break
2415+
}
2416+
2417+
time.Sleep(100 * time.Millisecond)
2418+
}
2419+
2420+
So(resp.StatusCode(), ShouldNotEqual, http.StatusOK)
2421+
2422+
err = os.WriteFile(ldapConfigPath, []byte(ldapConfigContent), 0o600)
2423+
So(err, ShouldBeNil)
2424+
2425+
for i := 0; i < 10; i++ {
2426+
// test if the credentials don't work
2427+
resp, _ = resty.R().SetBasicAuth(username, password).Get(baseURL + "/v2/")
2428+
So(resp, ShouldNotBeNil)
2429+
if resp.StatusCode() == http.StatusOK {
2430+
break
2431+
}
2432+
2433+
time.Sleep(100 * time.Millisecond)
2434+
}
2435+
2436+
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
2437+
2438+
// change the file
2439+
changedDir := t.TempDir()
2440+
changedLdapConfigPath := filepath.Join(changedDir, "ldap.json")
2441+
2442+
err = os.WriteFile(changedLdapConfigPath, []byte(newLdapConfigContent), 0o600)
2443+
So(err, ShouldBeNil)
2444+
2445+
configStr = fmt.Sprintf(configTemplate,
2446+
tempDir, "127.0.0.1", port, changedLdapConfigPath, LDAPAddress, ldapPort)
2447+
2448+
err = os.WriteFile(configPath, []byte(configStr), 0o600)
2449+
So(err, ShouldBeNil)
2450+
2451+
for i := 0; i < 10; i++ {
2452+
// test if the credentials don't work
2453+
resp, _ = resty.R().SetBasicAuth(username, password).Get(baseURL + "/v2/")
2454+
So(resp, ShouldNotBeNil)
2455+
2456+
if resp.StatusCode() != http.StatusOK {
2457+
break
2458+
}
2459+
2460+
time.Sleep(100 * time.Millisecond)
2461+
}
2462+
2463+
So(resp.StatusCode(), ShouldNotEqual, http.StatusOK)
2464+
2465+
err = os.WriteFile(changedLdapConfigPath, []byte(ldapConfigContent), 0o600)
2466+
So(err, ShouldBeNil)
2467+
2468+
for i := 0; i < 10; i++ {
2469+
// test if the credentials don't work
2470+
resp, _ = resty.R().SetBasicAuth(username, password).Get(baseURL + "/v2/")
2471+
So(resp, ShouldNotBeNil)
2472+
if resp.StatusCode() == http.StatusOK {
2473+
break
2474+
}
2475+
2476+
time.Sleep(100 * time.Millisecond)
2477+
}
2478+
2479+
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
2480+
2481+
// make it panic
2482+
badLDAPFilePath := filepath.Join(changedDir, "ldap222.json")
2483+
2484+
err = os.WriteFile(badLDAPFilePath, []byte(newLdapConfigContent), 0o600)
2485+
So(err, ShouldBeNil)
2486+
2487+
configStr = fmt.Sprintf(configTemplate,
2488+
tempDir, "127.0.0.1", port, changedLdapConfigPath, LDAPAddress, ldapPort)
2489+
2490+
err = os.WriteFile(configPath, []byte(configStr), 0o600)
2491+
So(err, ShouldBeNil)
2492+
2493+
// Loading the config should fail because the file doesn't exist so the old credentials
2494+
// are still up and working fine.
2495+
2496+
for i := 0; i < 10; i++ {
2497+
// test if the credentials don't work
2498+
resp, _ = resty.R().SetBasicAuth(username, password).Get(baseURL + "/v2/")
2499+
So(resp, ShouldNotBeNil)
2500+
if resp.StatusCode() == http.StatusOK {
2501+
break
2502+
}
2503+
2504+
time.Sleep(100 * time.Millisecond)
2505+
}
2506+
2507+
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
2508+
})
2509+
}
2510+
23342511
func TestLDAPWithoutCreds(t *testing.T) {
23352512
Convey("Make a new LDAP server", t, func() {
23362513
l := newTestLDAPServer()

pkg/cli/server/config_reloader.go

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package server
22

33
import (
4+
"errors"
45
"os"
56
"os/signal"
67
"syscall"
@@ -13,22 +14,24 @@ import (
1314
)
1415

1516
type HotReloader struct {
16-
watcher *fsnotify.Watcher
17-
filePath string
18-
ctlr *api.Controller
17+
watcher *fsnotify.Watcher
18+
configPath string
19+
ldapCredentialsPath string
20+
ctlr *api.Controller
1921
}
2022

21-
func NewHotReloader(ctlr *api.Controller, filePath string) (*HotReloader, error) {
23+
func NewHotReloader(ctlr *api.Controller, filePath, ldapCredentialsPath string) (*HotReloader, error) {
2224
// creates a new file watcher
2325
watcher, err := fsnotify.NewWatcher()
2426
if err != nil {
2527
return nil, err
2628
}
2729

2830
hotReloader := &HotReloader{
29-
watcher: watcher,
30-
filePath: filePath,
31-
ctlr: ctlr,
31+
watcher: watcher,
32+
configPath: filePath,
33+
ldapCredentialsPath: ldapCredentialsPath,
34+
ctlr: ctlr,
3235
}
3336

3437
return hotReloader, nil
@@ -73,13 +76,27 @@ func (hr *HotReloader) Start() {
7376

7477
newConfig := config.New()
7578

76-
err := LoadConfiguration(newConfig, hr.filePath)
79+
err := LoadConfiguration(newConfig, hr.configPath)
7780
if err != nil {
7881
log.Error().Err(err).Msg("failed to reload config, retry writing it.")
7982

8083
continue
8184
}
8285

86+
if hr.ctlr.Config.HTTP.Auth != nil && hr.ctlr.Config.HTTP.Auth.LDAP != nil &&
87+
hr.ctlr.Config.HTTP.Auth.LDAP.CredentialsFile != newConfig.HTTP.Auth.LDAP.CredentialsFile {
88+
err = hr.watcher.Remove(hr.ctlr.Config.HTTP.Auth.LDAP.CredentialsFile)
89+
if err != nil && !errors.Is(err, fsnotify.ErrNonExistentWatch) {
90+
log.Error().Err(err).Msg("failed to remove old watch for the credentials file")
91+
}
92+
93+
err = hr.watcher.Add(newConfig.HTTP.Auth.LDAP.CredentialsFile)
94+
if err != nil {
95+
log.Panic().Err(err).Str("ldap-credentials-file", newConfig.HTTP.Auth.LDAP.CredentialsFile).
96+
Msg("failed to watch ldap credentials file")
97+
}
98+
}
99+
83100
// stop background tasks gracefully
84101
hr.ctlr.StopBackgroundTasks()
85102

@@ -91,13 +108,20 @@ func (hr *HotReloader) Start() {
91108
}
92109
// watch for errors
93110
case err := <-hr.watcher.Errors:
94-
log.Panic().Err(err).Str("config", hr.filePath).Msg("fsnotfy error while watching config")
111+
log.Panic().Err(err).Str("config", hr.configPath).Msg("fsnotfy error while watching config")
95112
}
96113
}
97114
}()
98115

99-
if err := hr.watcher.Add(hr.filePath); err != nil {
100-
log.Panic().Err(err).Str("config", hr.filePath).Msg("failed to add config file to fsnotity watcher")
116+
if err := hr.watcher.Add(hr.configPath); err != nil {
117+
log.Panic().Err(err).Str("config", hr.configPath).Msg("failed to add config file to fsnotity watcher")
118+
}
119+
120+
if hr.ldapCredentialsPath != "" {
121+
if err := hr.watcher.Add(hr.ldapCredentialsPath); err != nil {
122+
log.Panic().Err(err).Str("ldap-credentials", hr.ldapCredentialsPath).
123+
Msg("failed to add ldap-credentials to fsnotity watcher")
124+
}
101125
}
102126

103127
<-done

pkg/cli/server/root.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,13 @@ func newServeCmd(conf *config.Config) *cobra.Command {
5555

5656
ctlr := api.NewController(conf)
5757

58+
ldapCredentials := ""
59+
60+
if conf.HTTP.Auth != nil && conf.HTTP.Auth.LDAP != nil {
61+
ldapCredentials = conf.HTTP.Auth.LDAP.CredentialsFile
62+
}
5863
// config reloader
59-
hotReloader, err := NewHotReloader(ctlr, args[0])
64+
hotReloader, err := NewHotReloader(ctlr, args[0], ldapCredentials)
6065
if err != nil {
6166
ctlr.Log.Error().Err(err).Msg("failed to create a new hot reloader")
6267

pkg/extensions/sync/sync_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1898,7 +1898,7 @@ func TestConfigReloader(t *testing.T) {
18981898
_, err = cfgfile.WriteString(content)
18991899
So(err, ShouldBeNil)
19001900

1901-
hotReloader, err := cli.NewHotReloader(dctlr, cfgfile.Name())
1901+
hotReloader, err := cli.NewHotReloader(dctlr, cfgfile.Name(), "")
19021902
So(err, ShouldBeNil)
19031903

19041904
hotReloader.Start()
@@ -2048,7 +2048,7 @@ func TestConfigReloader(t *testing.T) {
20482048
_, err = cfgfile.WriteString(content)
20492049
So(err, ShouldBeNil)
20502050

2051-
hotReloader, err := cli.NewHotReloader(dctlr, cfgfile.Name())
2051+
hotReloader, err := cli.NewHotReloader(dctlr, cfgfile.Name(), "")
20522052
So(err, ShouldBeNil)
20532053

20542054
hotReloader.Start()

0 commit comments

Comments
 (0)