Enable basic auth on gateway

Fixes https://github.com/openfaas/faas/issues/687 allowing the
gateway to handle the responsibility of basic auth for when it is
in use.

To enable set basic_auth env-var to true and then mount two
secrets or plaintext files under /var/secrets/

basic_auth_user, basic_auth_password

Tested with faas-cli list/deploy and with Safari browser.

Signed-off-by: Alex Ellis (VMware) <alexellis2@gmail.com>
This commit is contained in:
Alex Ellis (VMware)
2018-05-21 22:05:39 +01:00
committed by Alex Ellis
parent 8008a51b0b
commit a38931ce69
5 changed files with 167 additions and 3 deletions

View File

@ -0,0 +1,29 @@
package handlers
import (
"net/http"
)
// DecorateWithBasicAuth enforces basic auth as a middleware with given credentials
func DecorateWithBasicAuth(next http.HandlerFunc, credentials *BasicAuthCredentials) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, password, ok := r.BasicAuth()
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
if !ok || !(credentials.Password == password && user == credentials.User) {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("invalid credentials"))
return
}
next.ServeHTTP(w, r)
}
}
// BasicAuthCredentials for credentials
type BasicAuthCredentials struct {
User string
Password string
}

View File

@ -0,0 +1,63 @@
package handlers
import (
"io"
"net/http"
"net/http/httptest"
"testing"
)
func Test_AuthWithValidPassword_Gives200(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "<html><body>Hello World!</body></html>")
}
w := httptest.NewRecorder()
wantUser := "admin"
wantPassword := "password"
r := httptest.NewRequest(http.MethodGet, "http://localhost:8080", nil)
r.SetBasicAuth(wantUser, wantPassword)
wantCredentials := &BasicAuthCredentials{
User: wantUser,
Password: wantPassword,
}
decorated := DecorateWithBasicAuth(handler, wantCredentials)
decorated.ServeHTTP(w, r)
wantCode := http.StatusOK
if w.Code != wantCode {
t.Errorf("status code, want: %d, got: %d", wantCode, w.Code)
t.Fail()
}
}
func Test_AuthWithInvalidPassword_Gives403(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "<html><body>Hello World!</body></html>")
}
w := httptest.NewRecorder()
wantUser := "admin"
wantPassword := "test"
r := httptest.NewRequest(http.MethodGet, "http://localhost:8080", nil)
r.SetBasicAuth(wantUser, wantPassword)
wantCredentials := &BasicAuthCredentials{
User: wantUser,
Password: "",
}
decorated := DecorateWithBasicAuth(handler, wantCredentials)
decorated.ServeHTTP(w, r)
wantCode := http.StatusUnauthorized
if w.Code != wantCode {
t.Errorf("status code, want: %d, got: %d", wantCode, w.Code)
t.Fail()
}
}

View File

@ -5,8 +5,10 @@ package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"time"
"github.com/gorilla/mux"
@ -33,6 +35,27 @@ func main() {
log.Printf("Binding to external function provider: %s", config.FunctionsProviderURL)
var credentials *handlers.BasicAuthCredentials
if config.UseBasicAuth {
userPath := "/var/secrets/basic_auth_user"
user, userErr := ioutil.ReadFile(userPath)
if userErr != nil {
log.Panicf("Unable to load %s", userPath)
}
userPassword := "/var/secrets/basic_auth_password"
password, passErr := ioutil.ReadFile(userPassword)
if passErr != nil {
log.Panicf("Unable to load %s", userPassword)
}
credentials = &handlers.BasicAuthCredentials{
User: strings.TrimSpace(string(user)),
Password: strings.TrimSpace(string(password)),
}
}
metricsOptions := metrics.BuildMetricsOptions()
metrics.RegisterMetrics(metricsOptions)
@ -84,8 +107,20 @@ func main() {
}
prometheusQuery := metrics.NewPrometheusQuery(config.PrometheusHost, config.PrometheusPort, &http.Client{})
listFunctions := metrics.AddMetricsHandler(faasHandlers.ListFunctions, prometheusQuery)
faasHandlers.ListFunctions = metrics.AddMetricsHandler(faasHandlers.ListFunctions, prometheusQuery)
faasHandlers.Proxy = handlers.MakeCallIDMiddleware(faasHandlers.Proxy)
if credentials != nil {
faasHandlers.UpdateFunction =
handlers.DecorateWithBasicAuth(faasHandlers.UpdateFunction, credentials)
faasHandlers.DeleteFunction =
handlers.DecorateWithBasicAuth(faasHandlers.DeleteFunction, credentials)
faasHandlers.DeployFunction =
handlers.DecorateWithBasicAuth(faasHandlers.DeployFunction, credentials)
faasHandlers.ListFunctions =
handlers.DecorateWithBasicAuth(faasHandlers.ListFunctions, credentials)
}
r := mux.NewRouter()
// r.StrictSlash(false) // This didn't work, so register routes twice.
@ -97,7 +132,7 @@ func main() {
r.HandleFunc("/system/alert", faasHandlers.Alert)
r.HandleFunc("/system/function/{name:[-a-zA-Z_0-9]+}", queryFunction).Methods(http.MethodGet)
r.HandleFunc("/system/functions", listFunctions).Methods(http.MethodGet)
r.HandleFunc("/system/functions", faasHandlers.ListFunctions).Methods(http.MethodGet)
r.HandleFunc("/system/functions", faasHandlers.DeployFunction).Methods(http.MethodPost)
r.HandleFunc("/system/functions", faasHandlers.DeleteFunction).Methods(http.MethodDelete)
r.HandleFunc("/system/functions", faasHandlers.UpdateFunction).Methods(http.MethodPut)
@ -115,7 +150,12 @@ func main() {
allowedCORSHost := "raw.githubusercontent.com"
fsCORS := handlers.DecorateWithCORS(fs, allowedCORSHost)
r.PathPrefix("/ui/").Handler(http.StripPrefix("/ui", fsCORS)).Methods(http.MethodGet)
uiHandler := http.StripPrefix("/ui", fsCORS)
if credentials != nil {
r.PathPrefix("/ui/").Handler(handlers.DecorateWithBasicAuth(uiHandler.ServeHTTP, credentials)).Methods(http.MethodGet)
} else {
r.PathPrefix("/ui/").Handler(uiHandler).Methods(http.MethodGet)
}
metricsHandler := metrics.PrometheusHandler()
r.Handle("/metrics", metricsHandler)

View File

@ -105,6 +105,8 @@ func (ReadConfig) Read(hasEnv HasEnv) GatewayConfig {
cfg.DirectFunctions = parseBoolValue(hasEnv.Getenv("direct_functions"))
cfg.DirectFunctionsSuffix = hasEnv.Getenv("direct_functions_suffix")
cfg.UseBasicAuth = parseBoolValue(hasEnv.Getenv("basic_auth"))
return cfg
}
@ -140,6 +142,9 @@ type GatewayConfig struct {
// If set this will be used to resolve functions directly
DirectFunctionsSuffix string
// If set, reads secrets from file-system for enabling basic auth.
UseBasicAuth bool
}
// UseNATS Use NATSor not

View File

@ -197,3 +197,30 @@ func TestRead_PrometheusDefaults(t *testing.T) {
t.Fail()
}
}
func TestRead_BasicAuthDefaults(t *testing.T) {
defaults := NewEnvBucket()
readConfig := ReadConfig{}
config := readConfig.Read(defaults)
if config.UseBasicAuth != false {
t.Logf("config.UseBasicAuth, want: %t, got: %t\n", false, config.UseBasicAuth)
t.Fail()
}
}
func TestRead_BasicAuth_SetTrue(t *testing.T) {
defaults := NewEnvBucket()
defaults.Setenv("basic_auth", "true")
readConfig := ReadConfig{}
config := readConfig.Read(defaults)
if config.UseBasicAuth != true {
t.Logf("config.UseBasicAuth, want: %t, got: %t\n", true, config.UseBasicAuth)
t.Fail()
}
}