Files
zot/pkg/api/htpasswd.go
T
Luca Muscariello 2402296e9a fix: migrate to Go module v2 for proper semantic versioning (#3462)
* fix: migrate to Go module v2 for proper semantic versioning

This change updates the module path from 'zotregistry.dev/zot' to
'zotregistry.dev/zot/v2' to comply with Go's semantic versioning rules.

According to Go's module versioning requirements, major version v2+
must include the major version in the module path. The current
module path 'zotregistry.dev/zot' only supports v0.x.x and v1.x.x
versions, making existing v2.x.x tags (like v2.1.8) unusable.

Changes:
- Updated go.mod module path to zotregistry.dev/zot/v2
- Updated all internal import paths across 280+ Go source files
- Updated configuration files (golangcilint.yaml, gqlgen.yml)
- Updated README.md Go reference badge

This fix enables proper use of existing v2.x.x Git tags and allows
external packages to import zot v2+ versions without compatibility
errors.

Resolves: Go module import compatibility for v2+ versions
Fixes: #3071
Signed-off-by: Luca Muscariello <muscariello@ieee.org>

* fix: regenerate GraphQL files with updated v2 import paths

The gqlgen tool needs to regenerate the GraphQL schema files after
the module path change to use the new v2 imports.

Signed-off-by: Luca Muscariello <muscariello@ieee.org>

---------

Signed-off-by: Luca Muscariello <muscariello@ieee.org>
2025-10-16 22:43:47 -07:00

199 lines
4.1 KiB
Go

package api
import (
"bufio"
"context"
"errors"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"github.com/fsnotify/fsnotify"
"golang.org/x/crypto/bcrypt"
"zotregistry.dev/zot/v2/pkg/log"
)
// HTPasswd user auth store
//
// Currently supports only bcrypt hashes.
type HTPasswd struct {
mu sync.RWMutex
credMap map[string]string
log log.Logger
}
func NewHTPasswd(log log.Logger) *HTPasswd {
return &HTPasswd{
credMap: make(map[string]string),
log: log,
}
}
func (s *HTPasswd) Reload(filePath string) error {
credMap := make(map[string]string)
credsFile, err := os.Open(filePath)
if err != nil {
s.log.Error().Err(err).Str("htpasswd-file", filePath).Msg("failed to reload htpasswd")
return err
}
defer credsFile.Close()
scanner := bufio.NewScanner(credsFile)
for scanner.Scan() {
user, hash, ok := strings.Cut(scanner.Text(), ":")
if ok {
credMap[user] = hash
}
}
if len(credMap) == 0 {
s.log.Warn().Str("htpasswd-file", filePath).Msg("loaded htpasswd file appears to have zero users")
} else {
s.log.Info().Str("htpasswd-file", filePath).Int("users", len(credMap)).Msg("loaded htpasswd file")
}
s.mu.Lock()
defer s.mu.Unlock()
s.credMap = credMap
return nil
}
func (s *HTPasswd) Get(username string) (passphraseHash string, present bool) { //nolint: nonamedreturns
s.mu.RLock()
defer s.mu.RUnlock()
passphraseHash, present = s.credMap[username]
return
}
func (s *HTPasswd) Clear() {
s.mu.Lock()
defer s.mu.Unlock()
s.credMap = make(map[string]string)
}
func (s *HTPasswd) Authenticate(username, passphrase string) (ok, present bool) { //nolint: nonamedreturns
passphraseHash, present := s.Get(username)
if !present {
return false, false
}
err := bcrypt.CompareHashAndPassword([]byte(passphraseHash), []byte(passphrase))
ok = err == nil
if err != nil && !errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
// Log that user's hash has unsupported format. Better than silently return 401.
s.log.Warn().Err(err).Str("username", username).Msg("htpasswd bcrypt compare failed")
}
return
}
// HTPasswdWatcher helper which triggers htpasswd reload on file change event.
//
// Cannot be restarted.
type HTPasswdWatcher struct {
htp *HTPasswd
filePath string
watcher *fsnotify.Watcher
cancel context.CancelFunc
log log.Logger
}
// NewHTPasswdWatcher create and start watcher.
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,
cancel: cancel,
log: htp.log,
}
go func() {
defer ret.watcher.Close() //nolint: errcheck
for {
select {
case ev := <-ret.watcher.Events:
if ev.Op != fsnotify.Write {
continue
}
ret.log.Info().Str("htpasswd-file", ret.filePath).Msg("htpasswd file changed, trying to reload config")
err := ret.htp.Reload(ret.filePath)
if err != nil {
ret.log.Warn().Err(err).Str("htpasswd-file", ret.filePath).Msg("failed to reload file")
}
case err := <-ret.watcher.Errors:
ret.log.Error().Err(err).Str("htpasswd-file", ret.filePath).Msg("failed to fsnotfy, got error while watching file")
case <-ctx.Done():
ret.log.Debug().Msg("htpasswd watcher terminating...")
return
}
}
}()
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)
}
func (s *HTPasswdWatcher) Close() error {
s.cancel()
return nil
}