Skip to content

Commit

Permalink
feat(htpasswd): use dedicated fsnotify reloader for htpasswd file
Browse files Browse the repository at this point in the history
Signed-off-by: Vladimir Ermakov <vooon341@gmail.com>
  • Loading branch information
vooon committed Feb 8, 2025
1 parent 259df17 commit 8db17b2
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 37 deletions.
35 changes: 30 additions & 5 deletions pkg/api/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type Controller struct {
RelyingParties map[string]rp.RelyingParty
CookieStore *CookieStore
HTPasswd *HTPasswd
HTPasswdWatcher *HTPasswdWatcher
LDAPClient *LDAPClient
taskScheduler *scheduler.Scheduler
// runtime params
Expand Down Expand Up @@ -99,9 +100,17 @@ func NewController(appConfig *config.Config) *Controller {
Str("clusterMemberIndex", strconv.Itoa(memberSocketIdx)).Logger()
}

htp := NewHTPasswd(logger)

htw, err := NewHTPasswdWatcher(htp, "")
if err != nil {
logger.Panic().Err(err).Msg("failed to create htpasswd watcher")
}

controller.Config = appConfig
controller.Log = logger
controller.HTPasswd = NewHTPasswd(logger)
controller.HTPasswd = htp
controller.HTPasswdWatcher = htw

if appConfig.Log.Audit != "" {
audit := log.NewAuditLogger(appConfig.Log.Level, appConfig.Log.Audit)
Expand Down Expand Up @@ -285,6 +294,17 @@ func (c *Controller) Init() error {

c.InitCVEInfo()

if c.Config.IsHtpasswdAuthEnabled() {
err := c.HTPasswdWatcher.ChangeFile(c.Config.HTTP.Auth.HTPasswd.Path)
if err != nil {
return err
}
}

if err := c.HTPasswdWatcher.Start(); err != nil {
return err
}

return nil
}

Expand Down Expand Up @@ -367,10 +387,9 @@ func (c *Controller) LoadNewConfig(newConfig *config.Config) {
c.Config.HTTP.Auth.HTPasswd = newConfig.HTTP.Auth.HTPasswd
c.Config.HTTP.Auth.LDAP = newConfig.HTTP.Auth.LDAP

if c.Config.HTTP.Auth.HTPasswd.Path == "" {
c.HTPasswd.Clear()
} else {
_ = c.HTPasswd.Reload(c.Config.HTTP.Auth.HTPasswd.Path)
err := c.HTPasswdWatcher.ChangeFile(c.Config.HTTP.Auth.HTPasswd.Path)
if err != nil {
c.Log.Error().Err(err).Msg("failed to change watched htpasswd file")
}

if c.LDAPClient != nil {
Expand All @@ -379,6 +398,8 @@ func (c *Controller) LoadNewConfig(newConfig *config.Config) {
c.LDAPClient.BindPassword = newConfig.HTTP.Auth.LDAP.BindPassword()
c.LDAPClient.lock.Unlock()
}
} else {
_ = c.HTPasswdWatcher.ChangeFile("")
}

// reload periodical gc config
Expand Down Expand Up @@ -442,6 +463,10 @@ func (c *Controller) Shutdown() {
ctx := context.Background()
_ = c.Server.Shutdown(ctx)
}

if c.HTPasswdWatcher != nil {
_ = c.HTPasswdWatcher.Stop()
}
}

// Will stop scheduler and wait for all tasks to finish their work.
Expand Down
114 changes: 114 additions & 0 deletions pkg/api/htpasswd.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@ package api

import (
"bufio"
"context"
"errors"
"os"
"os/signal"
"strings"
"sync"
"syscall"

"github.com/fsnotify/fsnotify"
"golang.org/x/crypto/bcrypt"

"zotregistry.dev/zot/pkg/log"
)

// HTPasswd user auth store
//
// Currently supports only bcrypt hashes.
type HTPasswd struct {
mu sync.RWMutex
credMap map[string]string
Expand Down Expand Up @@ -83,3 +91,109 @@ func (s *HTPasswd) Authenticate(username, passphrase string) (ok, present bool)

return
}

// HTPasswdWatcher helper which triggers htpasswd reload on file change event.
//
// Cannot be restarted.
type HTPasswdWatcher struct {
htp *HTPasswd
filePath string
watcher *fsnotify.Watcher
ctx context.Context //nolint: containedctx
ccancel context.CancelFunc
log log.Logger
}

func NewHTPasswdWatcher(htp *HTPasswd, filePath string) (*HTPasswdWatcher, error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}

if filePath != "" {
err = watcher.Add(filePath)
if err != nil {
return nil, errors.Join(err, watcher.Close())
}
}

// background event processor job context
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)

ret := &HTPasswdWatcher{
htp: htp,
filePath: filePath,
watcher: watcher,
ctx: ctx,
ccancel: cancel,
log: htp.log,
}

return ret, nil
}

// ChangeFile changes monitored file. Empty string clears store.
func (s *HTPasswdWatcher) ChangeFile(filePath string) error {
if s.filePath != "" {
err := s.watcher.Remove(s.filePath)
if err != nil {
return err
}
}

if filePath == "" {
s.filePath = filePath
s.htp.Clear()

return nil
}

err := s.watcher.Add(filePath)
if err != nil {
return err
}

s.filePath = filePath

return s.htp.Reload(filePath)
}

// Start watcher. Can only be run once.
func (s *HTPasswdWatcher) Start() error {
go func() {
defer s.watcher.Close() //nolint: errcheck

for {
select {
case ev := <-s.watcher.Events:
if ev.Op != fsnotify.Write {
continue
}

s.log.Info().Str("htpasswd-file", s.filePath).Msg("htpasswd file changed, trying to reload config")

err := s.htp.Reload(s.filePath)
if err != nil {
s.log.Warn().Err(err).Str("htpasswd-file", s.filePath).Msg("failed to reload file")
}

case err := <-s.watcher.Errors:
s.log.Panic().Err(err).Str("htpasswd-file", s.filePath).Msg("fsnotfy error while watching config")

case <-s.ctx.Done():
s.log.Debug().Msg("htpasswd watcher terminating...")

return
}
}
}()

return nil
}

// Stop watcher. Can only be run once.
func (s *HTPasswdWatcher) Stop() error {
s.ccancel()

return nil
}
25 changes: 1 addition & 24 deletions pkg/cli/server/config_reloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,11 @@ import (
type HotReloader struct {
watcher *fsnotify.Watcher
configPath string
htpasswdPath string
ldapCredentialsPath string
ctlr *api.Controller
}

func NewHotReloader(ctlr *api.Controller, filePath, htpasswdPath, ldapCredentialsPath string) (*HotReloader, error) {
func NewHotReloader(ctlr *api.Controller, filePath, ldapCredentialsPath string) (*HotReloader, error) {
// creates a new file watcher
watcher, err := fsnotify.NewWatcher()
if err != nil {
Expand All @@ -31,7 +30,6 @@ func NewHotReloader(ctlr *api.Controller, filePath, htpasswdPath, ldapCredential
hotReloader := &HotReloader{
watcher: watcher,
configPath: filePath,
htpasswdPath: htpasswdPath,
ldapCredentialsPath: ldapCredentialsPath,
ctlr: ctlr,
}
Expand Down Expand Up @@ -85,20 +83,6 @@ func (hr *HotReloader) Start() {
continue
}

if hr.ctlr.Config.HTTP.Auth != nil &&
hr.ctlr.Config.HTTP.Auth.HTPasswd.Path != newConfig.HTTP.Auth.HTPasswd.Path {
err = hr.watcher.Remove(hr.ctlr.Config.HTTP.Auth.HTPasswd.Path)
if err != nil && !errors.Is(err, fsnotify.ErrNonExistentWatch) {
log.Error().Err(err).Msg("failed to remove old watch for the htpasswd file")
}

err = hr.watcher.Add(newConfig.HTTP.Auth.HTPasswd.Path)
if err != nil {
log.Panic().Err(err).Str("htpasswd-file", newConfig.HTTP.Auth.HTPasswd.Path).
Msg("failed to watch htpasswd file")
}
}

if hr.ctlr.Config.HTTP.Auth != nil && hr.ctlr.Config.HTTP.Auth.LDAP != nil &&
hr.ctlr.Config.HTTP.Auth.LDAP.CredentialsFile != newConfig.HTTP.Auth.LDAP.CredentialsFile {
err = hr.watcher.Remove(hr.ctlr.Config.HTTP.Auth.LDAP.CredentialsFile)
Expand Down Expand Up @@ -133,13 +117,6 @@ func (hr *HotReloader) Start() {
log.Panic().Err(err).Str("config", hr.configPath).Msg("failed to add config file to fsnotity watcher")
}

if hr.htpasswdPath != "" {
if err := hr.watcher.Add(hr.htpasswdPath); err != nil {
log.Panic().Err(err).Str("htpasswd-file", hr.htpasswdPath).
Msg("failed to add htpasswd to fsnotity watcher")
}
}

if hr.ldapCredentialsPath != "" {
if err := hr.watcher.Add(hr.ldapCredentialsPath); err != nil {
log.Panic().Err(err).Str("ldap-credentials", hr.ldapCredentialsPath).
Expand Down
7 changes: 1 addition & 6 deletions pkg/cli/server/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,13 @@ func newServeCmd(conf *config.Config) *cobra.Command {

ctlr := api.NewController(conf)

htpasswdPath := ""
ldapCredentials := ""

if conf.HTTP.Auth != nil {
htpasswdPath = conf.HTTP.Auth.HTPasswd.Path
}

if conf.HTTP.Auth != nil && conf.HTTP.Auth.LDAP != nil {
ldapCredentials = conf.HTTP.Auth.LDAP.CredentialsFile
}
// config reloader
hotReloader, err := NewHotReloader(ctlr, args[0], htpasswdPath, ldapCredentials)
hotReloader, err := NewHotReloader(ctlr, args[0], ldapCredentials)
if err != nil {
ctlr.Log.Error().Err(err).Msg("failed to create a new hot reloader")

Expand Down
4 changes: 2 additions & 2 deletions pkg/extensions/sync/sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2069,7 +2069,7 @@ func TestConfigReloader(t *testing.T) {
_, err = cfgfile.WriteString(content)
So(err, ShouldBeNil)

hotReloader, err := cli.NewHotReloader(dctlr, cfgfile.Name(), "", "")
hotReloader, err := cli.NewHotReloader(dctlr, cfgfile.Name(), "")
So(err, ShouldBeNil)

hotReloader.Start()
Expand Down Expand Up @@ -2219,7 +2219,7 @@ func TestConfigReloader(t *testing.T) {
_, err = cfgfile.WriteString(content)
So(err, ShouldBeNil)

hotReloader, err := cli.NewHotReloader(dctlr, cfgfile.Name(), "", "")
hotReloader, err := cli.NewHotReloader(dctlr, cfgfile.Name(), "")
So(err, ShouldBeNil)

hotReloader.Start()
Expand Down

0 comments on commit 8db17b2

Please sign in to comment.