Skip to content

Commit

Permalink
Add support for proxy-based authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
akmet committed Nov 1, 2024
1 parent e35c6e3 commit b8c42d3
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 25 deletions.
8 changes: 8 additions & 0 deletions changelog/unreleased/pull-307
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Enhancement: Add support for proxy-based authentication

The server now supports authentication via a proxy header specified with the --proxy-auth flag (e.g., --proxy-auth=X-Forwarded-User).
When this flag is set, the server will authenticate users based on the given header and disable BasicAuth.
Note that --proxy-auth is ignored if --no-auth is set, as --no-auth disables all authentication.

https://github.com/restic/rest-server/issues/174
https://github.com/restic/rest-server/pull/307
9 changes: 7 additions & 2 deletions cmd/rest-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ func newRestServerApp() *restServerApp {
flags.BoolVar(&rv.Server.TLS, "tls", rv.Server.TLS, "turn on TLS support")
flags.StringVar(&rv.Server.TLSCert, "tls-cert", rv.Server.TLSCert, "TLS certificate path")
flags.StringVar(&rv.Server.TLSKey, "tls-key", rv.Server.TLSKey, "TLS key path")
flags.BoolVar(&rv.Server.NoAuth, "no-auth", rv.Server.NoAuth, "disable .htpasswd authentication")
flags.BoolVar(&rv.Server.NoAuth, "no-auth", rv.Server.NoAuth, "disable authentication")
flags.StringVar(&rv.Server.HtpasswdPath, "htpasswd-file", rv.Server.HtpasswdPath, "location of .htpasswd file (default: \"<data directory>/.htpasswd)\"")
flags.StringVar(&rv.Server.ProxyAuthUsername, "proxy-auth-username", rv.Server.ProxyAuthUsername, "specifies the HTTP header containing the username for proxy-based authentication")
flags.BoolVar(&rv.Server.NoVerifyUpload, "no-verify-upload", rv.Server.NoVerifyUpload,
"do not verify the integrity of uploaded data. DO NOT enable unless the rest-server runs on a very low-power device")
flags.BoolVar(&rv.Server.AppendOnly, "append-only", rv.Server.AppendOnly, "enable append only mode")
Expand Down Expand Up @@ -127,7 +128,11 @@ func (app *restServerApp) runRoot(cmd *cobra.Command, args []string) error {
if app.Server.NoAuth {
log.Println("Authentication disabled")
} else {
log.Println("Authentication enabled")
if app.Server.ProxyAuthUsername == "" {
log.Println("Authentication enabled")
} else {
log.Println("Proxy Authentication enabled.")
}
}

handler, err := restserver.NewHandler(&app.Server)
Expand Down
6 changes: 6 additions & 0 deletions cmd/rest-server/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ func TestGetHandler(t *testing.T) {
t.Errorf("NoAuth=true: expected no error, got %v", err)
}

// With NoAuth = false, no .htpasswd and ProxyAuth = X-Remote-User
_, err = getHandler(&restserver.Server{Path: dir, ProxyAuthUsername: "X-Remote-User"})
if err != nil {
t.Errorf("NoAuth=false, ProxyAuthUsername = X-Remote-User: expected no error, got %v", err)
}

// With NoAuth = false and custom .htpasswd
htpFile, err := ioutil.TempFile(dir, "custom")
if err != nil {
Expand Down
37 changes: 19 additions & 18 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,24 @@ import (

// Server encapsulates the rest-server's settings and repo management logic
type Server struct {
Path string
HtpasswdPath string
Listen string
Log string
CPUProfile string
TLSKey string
TLSCert string
TLS bool
NoAuth bool
AppendOnly bool
PrivateRepos bool
Prometheus bool
PrometheusNoAuth bool
Debug bool
MaxRepoSize int64
PanicOnError bool
NoVerifyUpload bool
Path string
HtpasswdPath string
Listen string
Log string
CPUProfile string
TLSKey string
TLSCert string
TLS bool
NoAuth bool
ProxyAuthUsername string
AppendOnly bool
PrivateRepos bool
Prometheus bool
PrometheusNoAuth bool
Debug bool
MaxRepoSize int64
PanicOnError bool
NoVerifyUpload bool

htpasswdFile *HtpasswdFile
quotaManager *quota.Manager
Expand Down Expand Up @@ -158,7 +159,7 @@ func join(base string, names ...string) (string, error) {
// splitURLPath splits the URL path into a folderPath of the subrepo, and
// a remainder that can be passed to repo.Handler.
// Example: /foo/bar/locks/0123... will be split into:
// ["foo", "bar"] and "/locks/0123..."
// ["foo", "bar"] and "/locks/0123..."
func splitURLPath(urlPath string, maxDepth int) (folderPath []string, remainder string) {
if !strings.HasPrefix(urlPath, "/") {
// Really should start with "/"
Expand Down
17 changes: 12 additions & 5 deletions mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,17 @@ func (s *Server) checkAuth(r *http.Request) (username string, ok bool) {
if s.NoAuth {
return username, true
}
var password string
username, password, ok = r.BasicAuth()
if !ok || !s.htpasswdFile.Validate(username, password) {
return "", false
if s.ProxyAuthUsername != "" {
username = r.Header.Get(s.ProxyAuthUsername)
if username == "" {
return "", false
}
} else {
var password string
username, password, ok = r.BasicAuth()
if !ok || !s.htpasswdFile.Validate(username, password) {
return "", false
}
}
return username, true
}
Expand All @@ -66,7 +73,7 @@ func (s *Server) wrapMetricsAuth(f http.HandlerFunc) http.HandlerFunc {

// NewHandler returns the master HTTP multiplexer/router.
func NewHandler(server *Server) (http.Handler, error) {
if !server.NoAuth {
if !server.NoAuth && server.ProxyAuthUsername == "" {
var err error
if server.HtpasswdPath == "" {
server.HtpasswdPath = filepath.Join(server.Path, ".htpasswd")
Expand Down
72 changes: 72 additions & 0 deletions mux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package restserver

import (
"net/http/httptest"
"testing"
)

func TestCheckAuth(t *testing.T) {
tests := []struct {
name string
server *Server
requestHeaders map[string]string
basicAuth bool
basicUser string
basicPassword string
expectedUser string
expectedOk bool
}{
{
name: "NoAuth enabled",
server: &Server{
NoAuth: true,
},
expectedOk: true,
},
{
name: "Proxy Auth successful",
server: &Server{
ProxyAuthUsername: "X-Remote-User",
},
requestHeaders: map[string]string{
"X-Remote-User": "restic",
},
expectedUser: "restic",
expectedOk: true,
},
{
name: "Proxy Auth empty header",
server: &Server{
ProxyAuthUsername: "X-Remote-User",
},
requestHeaders: map[string]string{
"X-Remote-User": "",
},
expectedOk: false,
},
{
name: "Proxy Auth missing header",
server: &Server{
ProxyAuthUsername: "X-Remote-User",
},
expectedOk: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
for header, value := range tt.requestHeaders {
req.Header.Set(header, value)
}
if tt.basicAuth {
req.SetBasicAuth(tt.basicUser, tt.basicPassword)
}

username, ok := tt.server.checkAuth(req)
if username != tt.expectedUser || ok != tt.expectedOk {
t.Errorf("expected (%v, %v), got (%v, %v)", tt.expectedUser, tt.expectedOk, username, ok)
}
})
}
}

0 comments on commit b8c42d3

Please sign in to comment.