feat(authentication): file provider hot reload (#4188)

This adds hot reloading to the file auth provider.
pull/4189/head
James Elliott 2022-10-17 22:31:23 +11:00 committed by GitHub
parent 3a70f6739b
commit 84cb457cb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 165 additions and 23 deletions

View File

@ -20,6 +20,7 @@ aliases:
authentication_backend: authentication_backend:
file: file:
path: /config/users.yml path: /config/users.yml
watch: false
password: password:
algorithm: argon2 algorithm: argon2
argon2: argon2:
@ -58,6 +59,12 @@ The path to the file with the user details list. Supported file types are:
* [YAML File](../../reference/guides/passwords.md#yaml-format) * [YAML File](../../reference/guides/passwords.md#yaml-format)
### watch
{{< confkey type="boolean" default="false" required="no" >}}
Enables reloading the database by watching it for changes.
## Password Options ## Password Options
A [reference guide](../../reference/guides/passwords.md) exists specifically for choosing password hashing values. This A [reference guide](../../reference/guides/passwords.md) exists specifically for choosing password hashing values. This

View File

@ -2,7 +2,7 @@
title: "authelia crypto hash" title: "authelia crypto hash"
description: "Reference for the authelia crypto hash command." description: "Reference for the authelia crypto hash command."
lead: "" lead: ""
date: 2022-08-27T10:46:58+10:00 date: 2022-10-17T21:51:59+11:00
draft: false draft: false
images: [] images: []
menu: menu:

View File

@ -2,7 +2,7 @@
title: "authelia crypto hash generate" title: "authelia crypto hash generate"
description: "Reference for the authelia crypto hash generate command." description: "Reference for the authelia crypto hash generate command."
lead: "" lead: ""
date: 2022-08-27T10:46:58+10:00 date: 2022-10-17T21:51:59+11:00
draft: false draft: false
images: [] images: []
menu: menu:

View File

@ -2,7 +2,7 @@
title: "authelia crypto hash generate argon2" title: "authelia crypto hash generate argon2"
description: "Reference for the authelia crypto hash generate argon2 command." description: "Reference for the authelia crypto hash generate argon2 command."
lead: "" lead: ""
date: 2022-08-27T10:46:58+10:00 date: 2022-10-17T21:51:59+11:00
draft: false draft: false
images: [] images: []
menu: menu:

View File

@ -2,7 +2,7 @@
title: "authelia crypto hash generate bcrypt" title: "authelia crypto hash generate bcrypt"
description: "Reference for the authelia crypto hash generate bcrypt command." description: "Reference for the authelia crypto hash generate bcrypt command."
lead: "" lead: ""
date: 2022-08-27T10:46:58+10:00 date: 2022-10-17T21:51:59+11:00
draft: false draft: false
images: [] images: []
menu: menu:

View File

@ -2,7 +2,7 @@
title: "authelia crypto hash generate pbkdf2" title: "authelia crypto hash generate pbkdf2"
description: "Reference for the authelia crypto hash generate pbkdf2 command." description: "Reference for the authelia crypto hash generate pbkdf2 command."
lead: "" lead: ""
date: 2022-08-27T10:46:58+10:00 date: 2022-10-17T21:51:59+11:00
draft: false draft: false
images: [] images: []
menu: menu:

View File

@ -2,7 +2,7 @@
title: "authelia crypto hash generate scrypt" title: "authelia crypto hash generate scrypt"
description: "Reference for the authelia crypto hash generate scrypt command." description: "Reference for the authelia crypto hash generate scrypt command."
lead: "" lead: ""
date: 2022-08-27T10:46:58+10:00 date: 2022-10-17T21:51:59+11:00
draft: false draft: false
images: [] images: []
menu: menu:

View File

@ -2,7 +2,7 @@
title: "authelia crypto hash generate sha2crypt" title: "authelia crypto hash generate sha2crypt"
description: "Reference for the authelia crypto hash generate sha2crypt command." description: "Reference for the authelia crypto hash generate sha2crypt command."
lead: "" lead: ""
date: 2022-08-27T10:46:58+10:00 date: 2022-10-17T21:51:59+11:00
draft: false draft: false
images: [] images: []
menu: menu:

View File

@ -2,7 +2,7 @@
title: "authelia crypto hash validate" title: "authelia crypto hash validate"
description: "Reference for the authelia crypto hash validate command." description: "Reference for the authelia crypto hash validate command."
lead: "" lead: ""
date: 2022-08-27T10:46:58+10:00 date: 2022-10-17T21:51:59+11:00
draft: false draft: false
images: [] images: []
menu: menu:

View File

@ -2,7 +2,7 @@
title: "authelia crypto rand" title: "authelia crypto rand"
description: "Reference for the authelia crypto rand command." description: "Reference for the authelia crypto rand command."
lead: "" lead: ""
date: 2022-08-27T10:46:58+10:00 date: 2022-10-17T21:51:59+11:00
draft: false draft: false
images: [] images: []
menu: menu:

File diff suppressed because one or more lines are too long

2
go.mod
View File

@ -9,6 +9,7 @@ require (
github.com/duosecurity/duo_api_golang v0.0.0-20220927171823-f4576e85b96c github.com/duosecurity/duo_api_golang v0.0.0-20220927171823-f4576e85b96c
github.com/fasthttp/router v1.4.12 github.com/fasthttp/router v1.4.12
github.com/fasthttp/session/v2 v2.4.13 github.com/fasthttp/session/v2 v2.4.13
github.com/fsnotify/fsnotify v1.6.0
github.com/go-asn1-ber/asn1-ber v1.5.4 github.com/go-asn1-ber/asn1-ber v1.5.4
github.com/go-crypt/crypt v0.1.13 github.com/go-crypt/crypt v0.1.13
github.com/go-ldap/ldap/v3 v3.4.4 github.com/go-ldap/ldap/v3 v3.4.4
@ -56,7 +57,6 @@ require (
github.com/dlclark/regexp2 v1.4.0 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/go-crypt/x v0.1.3 // indirect github.com/go-crypt/x v0.1.3 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect

6
go.sum
View File

@ -229,8 +229,8 @@ github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVB
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@ -1715,12 +1715,12 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43 h1:OK7RB6t2WQX54srQQYSXMW8dF5C6/8+oA/s5QBmmto4= golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43 h1:OK7RB6t2WQX54srQQYSXMW8dF5C6/8+oA/s5QBmmto4=
golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=

View File

@ -87,8 +87,13 @@ const (
hashBCrypt = "bcrypt" hashBCrypt = "bcrypt"
) )
// ErrUserNotFound indicates the user wasn't found in the authentication backend. var (
var ErrUserNotFound = errors.New("user not found") // ErrUserNotFound indicates the user wasn't found in the authentication backend.
ErrUserNotFound = errors.New("user not found")
// ErrNoContent is returned when the file is empty.
ErrNoContent = errors.New("no file content")
)
const fileAuthenticationMode = 0600 const fileAuthenticationMode = 0600

View File

@ -2,8 +2,11 @@ package authentication
import ( import (
_ "embed" // Embed users_database.template.yml. _ "embed" // Embed users_database.template.yml.
"errors"
"fmt" "fmt"
"os" "os"
"sync"
"time"
"github.com/go-crypt/crypt" "github.com/go-crypt/crypt"
@ -13,18 +16,48 @@ import (
// FileUserProvider is a provider reading details from a file. // FileUserProvider is a provider reading details from a file.
type FileUserProvider struct { type FileUserProvider struct {
config *schema.FileAuthenticationBackend config *schema.FileAuthenticationBackend
hash crypt.Hash hash crypt.Hash
database *FileUserDatabase database *FileUserDatabase
mutex *sync.Mutex
timeoutReload time.Time
} }
// NewFileUserProvider creates a new instance of FileUserProvider. // NewFileUserProvider creates a new instance of FileUserProvider.
func NewFileUserProvider(config *schema.FileAuthenticationBackend) (provider *FileUserProvider) { func NewFileUserProvider(config *schema.FileAuthenticationBackend) (provider *FileUserProvider) {
return &FileUserProvider{ return &FileUserProvider{
config: config, config: config,
mutex: &sync.Mutex{},
timeoutReload: time.Now().Add(-1 * time.Second),
} }
} }
// Reload the database.
func (p *FileUserProvider) Reload() (reloaded bool, err error) {
now := time.Now()
p.mutex.Lock()
defer p.mutex.Unlock()
if now.Before(p.timeoutReload) {
return false, nil
}
switch err = p.database.Load(); {
case err == nil:
p.setTimeoutReload(now)
case errors.Is(err, ErrNoContent):
return false, nil
default:
return false, fmt.Errorf("failed to reload: %w", err)
}
p.setTimeoutReload(now)
return true, nil
}
// CheckUserPassword checks if provided password matches for the given user. // CheckUserPassword checks if provided password matches for the given user.
func (p *FileUserProvider) CheckUserPassword(username string, password string) (match bool, err error) { func (p *FileUserProvider) CheckUserPassword(username string, password string) (match bool, err error) {
var details DatabaseUserDetails var details DatabaseUserDetails
@ -73,6 +106,12 @@ func (p *FileUserProvider) UpdatePassword(username string, newPassword string) (
p.database.SetUserDetails(details.Username, &details) p.database.SetUserDetails(details.Username, &details)
p.mutex.Lock()
p.setTimeoutReload(time.Now())
p.mutex.Unlock()
if err = p.database.Save(); err != nil { if err = p.database.Save(); err != nil {
return err return err
} }
@ -101,6 +140,10 @@ func (p *FileUserProvider) StartupCheck() (err error) {
return nil return nil
} }
func (p *FileUserProvider) setTimeoutReload(now time.Time) {
p.timeoutReload = now.Add(time.Second / 2)
}
// NewFileCryptoHashFromConfig returns a crypt.Hash given a valid configuration. // NewFileCryptoHashFromConfig returns a crypt.Hash given a valid configuration.
func NewFileCryptoHashFromConfig(config schema.Password) (hash crypt.Hash, err error) { func NewFileCryptoHashFromConfig(config schema.Password) (hash crypt.Hash, err error) {
switch config.Algorithm { switch config.Algorithm {

View File

@ -7,7 +7,7 @@ import (
"github.com/asaskevich/govalidator" "github.com/asaskevich/govalidator"
"github.com/go-crypt/crypt" "github.com/go-crypt/crypt"
"gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
// NewFileUserDatabase creates a new FileUserDatabase. // NewFileUserDatabase creates a new FileUserDatabase.
@ -168,6 +168,10 @@ func (m *DatabaseModel) Read(filePath string) (err error) {
return fmt.Errorf("failed to read the '%s' file: %w", filePath, err) return fmt.Errorf("failed to read the '%s' file: %w", filePath, err)
} }
if len(content) == 0 {
return ErrNoContent
}
if err = yaml.Unmarshal(content, m); err != nil { if err = yaml.Unmarshal(content, m); err != nil {
return fmt.Errorf("could not parse the YAML database: %w", err) return fmt.Errorf("could not parse the YAML database: %w", err)
} }

View File

@ -6,14 +6,17 @@ import (
"net" "net"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"strings" "strings"
"syscall" "syscall"
"github.com/fsnotify/fsnotify"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/logging" "github.com/authelia/authelia/v4/internal/logging"
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
@ -83,11 +86,11 @@ func cmdRootRun(_ *cobra.Command, _ []string) {
doStartupChecks(config, &providers, logger) doStartupChecks(config, &providers, logger)
runServers(config, providers, logger) runServices(config, providers, logger)
} }
//nolint:gocyclo // Complexity is required in this function. //nolint:gocyclo // Complexity is required in this function.
func runServers(config *schema.Configuration, providers middlewares.Providers, log *logrus.Logger) { func runServices(config *schema.Configuration, providers middlewares.Providers, log *logrus.Logger) {
ctx := context.Background() ctx := context.Background()
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
@ -151,6 +154,15 @@ func runServers(config *schema.Configuration, providers middlewares.Providers, l
return nil return nil
}) })
if config.AuthenticationBackend.File != nil && config.AuthenticationBackend.File.Watch {
provider := providers.UserProvider.(*authentication.FileUserProvider)
if watcher, err := runServiceFileWatcher(g, log, config.AuthenticationBackend.File.Path, provider); err != nil {
log.WithError(err).Errorf("Error opening file watcher")
} else {
defer watcher.Close()
}
}
select { select {
case s := <-quit: case s := <-quit:
switch s { switch s {
@ -186,6 +198,75 @@ func runServers(config *schema.Configuration, providers middlewares.Providers, l
} }
} }
type ReloadFilter func(path string) (skipped bool)
type ProviderReload interface {
Reload() (reloaded bool, err error)
}
func runServiceFileWatcher(g *errgroup.Group, log *logrus.Logger, path string, reload ProviderReload) (watcher *fsnotify.Watcher, err error) {
if watcher, err = fsnotify.NewWatcher(); err != nil {
return nil, err
}
failed := make(chan struct{})
var directory, filename string
if path != "" {
directory, filename = filepath.Dir(path), filepath.Base(path)
}
g.Go(func() error {
for {
select {
case <-failed:
return nil
case event, ok := <-watcher.Events:
if !ok {
return nil
}
if filename != filepath.Base(event.Name) {
log.WithField("file", event.Name).WithField("op", event.Op).Tracef("File modification detected to irrelevant file")
break
}
switch {
case event.Op&fsnotify.Write == fsnotify.Write, event.Op&fsnotify.Create == fsnotify.Create:
log.WithField("file", event.Name).WithField("op", event.Op).Debug("File modification detected")
switch reloaded, err := reload.Reload(); {
case err != nil:
log.WithField("file", event.Name).WithField("op", event.Op).WithError(err).Error("Error occurred reloading file")
case reloaded:
log.WithField("file", event.Name).Info("Reloaded file successfully")
default:
log.WithField("file", event.Name).Debug("Reload of file was triggered but it was skipped")
}
case event.Op&fsnotify.Remove == fsnotify.Remove:
log.WithField("file", event.Name).WithField("op", event.Op).Debug("Remove of file was detected")
}
case err, ok := <-watcher.Errors:
if !ok {
return nil
}
log.WithError(err).Errorf("Error while watching files")
}
}
})
if err := watcher.Add(directory); err != nil {
failed <- struct{}{}
return nil, err
}
log.WithField("directory", directory).WithField("file", filename).Debug("Directory is being watched for changes to the file")
return watcher, nil
}
func doStartupChecks(config *schema.Configuration, providers *middlewares.Providers, log *logrus.Logger) { func doStartupChecks(config *schema.Configuration, providers *middlewares.Providers, log *logrus.Logger) {
var ( var (
failures []string failures []string

View File

@ -24,6 +24,7 @@ type PasswordResetAuthenticationBackend struct {
// FileAuthenticationBackend represents the configuration related to file-based backend. // FileAuthenticationBackend represents the configuration related to file-based backend.
type FileAuthenticationBackend struct { type FileAuthenticationBackend struct {
Path string `koanf:"path"` Path string `koanf:"path"`
Watch bool `koanf:"watch"`
Password Password `koanf:"password"` Password Password `koanf:"password"`
} }

View File

@ -50,6 +50,7 @@ var Keys = []string{
"authentication_backend.password_reset.custom_url", "authentication_backend.password_reset.custom_url",
"authentication_backend.refresh_interval", "authentication_backend.refresh_interval",
"authentication_backend.file.path", "authentication_backend.file.path",
"authentication_backend.file.watch",
"authentication_backend.file.password.algorithm", "authentication_backend.file.password.algorithm",
"authentication_backend.file.password.argon2.variant", "authentication_backend.file.password.argon2.variant",
"authentication_backend.file.password.argon2.iterations", "authentication_backend.file.password.argon2.iterations",