From 686916414ac771704f45a62bc62ca50aee81bbb5 Mon Sep 17 00:00:00 2001 From: Mike Futerko Date: Mon, 4 Mar 2019 23:13:11 +0200 Subject: [PATCH] Implement basic auth htpasswd file refresh (#604) --- auth/basic.go | 39 ++++++++++++++++++++++++--- config/config.go | 6 +++-- config/load.go | 17 ++++++++++-- docs/content/feature/authorization.md | 23 ++++++++-------- docs/content/ref/proxy.auth.md | 15 ++++++----- fabio.properties | 14 +++++++--- 6 files changed, 86 insertions(+), 28 deletions(-) diff --git a/auth/basic.go b/auth/basic.go index 405e526fe..438f13bdf 100644 --- a/auth/basic.go +++ b/auth/basic.go @@ -3,6 +3,8 @@ package auth import ( "log" "net/http" + "os" + "time" "github.com/fabiolb/fabio/config" "github.com/tg123/go-htpasswd" @@ -15,14 +17,45 @@ type basic struct { } func newBasicAuth(cfg config.BasicAuth) (AuthScheme, error) { - secrets, err := htpasswd.New(cfg.File, htpasswd.DefaultSystems, func(err error) { - log.Println("[WARN] Error reading Htpasswd file: ", err) - }) + bad := func(err error) { + log.Println("[WARN] Error processing a line in an htpasswd file:", err) + } + + stat, err := os.Stat(cfg.File) + if err != nil { + return nil, err + } + cfg.ModTime = stat.ModTime() + secrets, err := htpasswd.New(cfg.File, htpasswd.DefaultSystems, bad) if err != nil { return nil, err } + if cfg.Refresh > 0 { + go func() { + ticker := time.NewTicker(cfg.Refresh).C + for range ticker { + stat, err := os.Stat(cfg.File) + if err != nil { + log.Println("[WARN] Error accessing htpasswd file:", err) + continue // to prevent nil pointer dereference below + } + + // refresh the htpasswd file only if its modification time has changed + // even if the new htpasswd file is older than previously loaded + if cfg.ModTime != stat.ModTime() { + if err := secrets.Reload(bad); err == nil { + log.Println("[INFO] The htpasswd file has been successfully reloaded") + cfg.ModTime = stat.ModTime() + } else { + log.Println("[WARN] Error reloading htpasswd file:", err) + } + } + } + }() + } + return &basic{ secrets: secrets, realm: cfg.Realm, diff --git a/config/config.go b/config/config.go index 4c5a038ee..5c84c8ad5 100644 --- a/config/config.go +++ b/config/config.go @@ -170,6 +170,8 @@ type AuthScheme struct { } type BasicAuth struct { - Realm string - File string + Realm string + File string + Refresh time.Duration + ModTime time.Time // the htpasswd file last modification time } diff --git a/config/load.go b/config/load.go index 11f9b4d0c..3f0a71f75 100644 --- a/config/load.go +++ b/config/load.go @@ -613,8 +613,9 @@ func parseAuthScheme(cfg map[string]string) (a AuthScheme, err error) { return AuthScheme{}, fmt.Errorf("missing 'type' in auth '%s'", a.Name) case "basic": a.Basic = BasicAuth{ - File: cfg["file"], - Realm: cfg["realm"], + File: cfg["file"], + Realm: cfg["realm"], + Refresh: 0, // the htpasswd file refresh is disabled by default } if a.Basic.File == "" { @@ -623,6 +624,18 @@ func parseAuthScheme(cfg map[string]string) (a AuthScheme, err error) { if a.Basic.Realm == "" { a.Basic.Realm = a.Name } + + if cfg["refresh"] != "" { + d, err := time.ParseDuration(cfg["refresh"]) + if err != nil { + return AuthScheme{}, err + } + if d < time.Second { + d = time.Second + } + a.Basic.Refresh = d + } + default: return AuthScheme{}, fmt.Errorf("unknown auth type '%s'", a.Type) } diff --git a/docs/content/feature/authorization.md b/docs/content/feature/authorization.md index 262d4b949..28560260a 100644 --- a/docs/content/feature/authorization.md +++ b/docs/content/feature/authorization.md @@ -23,32 +23,33 @@ referenced in a route configuration. When you configure the route, you must reference the unique name for the authorization scheme: route add svc / https://127.0.0.1:8080 auth= - + urlprefix-/ proto=https auth= - + The following types of authorization schemes are available: - * [`basic`](#basic): legacy store for a single TLS and a set of client auth certificates - +* [`basic`](#basic): legacy store for a single TLS and a set of client auth certificates + At the end you also find a list of [examples](#examples). ### Basic The basic authorization scheme leverages [Http Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication) and reads a [htpasswd](https://httpd.apache.org/docs/2.4/misc/password_encryptions.html) file at startup and credentials are cached until the service exits. -The `file` option contains the path to the htpasswd file. The `realm` parameter is optional (default is to use the `name`) +The `file` option contains the path to the htpasswd file. The `realm` parameter is optional (default is to use the `name`). The `refresh` option can set the htpasswd file refresh interval. Minimal refresh interval is `1s` to void busy loop. By default refresh is disabled i.e. set to zero. - name=;type=basic;file=;realm= + name=;type=basic;file=;realm=;refresh= Supported htpasswd formats are detailed [here](https://github.com/tg123/go-htpasswd) -##### Examples +#### Examples - # single basic auth scheme - + # single basic auth scheme name=mybasicauth;type=basic;file=p/creds.htpasswd; - # basic auth with multiple schemes + # single basic auth scheme with refresh interval set to 30 seconds + name=mybasicauth;type=basic;file=p/creds.htpasswd;refresh=30s - proxy.auth = name=mybasicauth;type=basic;file=p/creds.htpasswd + # basic auth with multiple schemes + proxy.auth = name=mybasicauth;type=basic;file=p/creds.htpasswd;refresh=30s name=myotherauth;type=basic;file=p/other-creds.htpasswd;realm=myrealm \ No newline at end of file diff --git a/docs/content/ref/proxy.auth.md b/docs/content/ref/proxy.auth.md index 244cce7d0..ee2b41d4b 100644 --- a/docs/content/ref/proxy.auth.md +++ b/docs/content/ref/proxy.auth.md @@ -17,23 +17,24 @@ The following types of authorization schemes are available: The basic authorization scheme leverages [Http Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication) and reads a [htpasswd](https://httpd.apache.org/docs/2.4/misc/password_encryptions.html) file at startup and credentials are cached until the service exits. -The `file` option contains the path to the htpasswd file. The `realm` parameter is optional (default is to use the `name`) +The `file` option contains the path to the htpasswd file. The `realm` parameter is optional (default is to use the `name`). The `refresh` option can set the htpasswd file refresh interval. Minimal refresh interval is `1s` to void busy loop. By default refresh is disabled i.e. set to zero. - name=;type=basic;file=;realm= + name=;type=basic;file=;realm=;refresh= Supported htpasswd formats are detailed [here](https://github.com/tg123/go-htpasswd) #### Examples # single basic auth scheme - name=mybasicauth;type=basic;file=p/creds.file; + # single basic auth scheme with refresh interval set to 30 seconds + name=mybasicauth;type=basic;file=p/creds.htpasswd;refresh=30s + # basic auth with multiple schemes - - proxy.auth = name=mybasicauth;type=basic;file=p/creds.htpasswd + proxy.auth = name=mybasicauth;type=basic;file=p/creds.htpasswd;refresh=30s name=myotherauth;type=basic;file=p/other-creds.htpasswd;realm=myrealm -The default is - proxy.auth = +The default is + proxy.auth = \ No newline at end of file diff --git a/fabio.properties b/fabio.properties index 95bde5855..52e2e8d0a 100644 --- a/fabio.properties +++ b/fabio.properties @@ -481,11 +481,15 @@ # Basic # # The basic auth scheme leverages http basic authentication using -# one htpasswd file which is loaded at startup and is cached until -# the service exits. +# one htpasswd file which is loaded at startup and by default is cached until +# the service exits. However, it's possible to refresh htpasswd file +# periodically by setting the refresh interval with 'refresh' option. # # The 'file' option contains the path to the htpasswd file. The 'realm' -# option contains realm name (optional, default is the scheme name +# option contains realm name (optional, default is the scheme name). +# The 'refresh' option can set the htpasswd file refresh interval. Minimal +# refresh interval is 1s to void busy loop. +# By default refresh is disabled i.e. set to zero. # # name=;type=basic;file=p/creds.htpasswd;realm=foo # @@ -495,6 +499,10 @@ # # name=mybasicauth;type=basic;file=p/creds.htpasswd; # +# # single basic auth scheme with refresh interval set to 30 seconds +# +# name=mybasicauth;type=basic;file=p/creds.htpasswd;refresh=30s +# # # basic auth with multiple schemes # # proxy.auth = name=mybasicauth;type=basic;file=p/creds.htpasswd