From 576cd71a443e58cdbdb82341891069f1f3862a77 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Thu, 14 Nov 2024 13:49:30 +0100 Subject: [PATCH] Auth: disable via cli flag (#17249) --- cmd/flags.go | 3 +++ cmd/root.go | 11 ++++++++++- server/http.go | 3 +-- server/http_auth.go | 10 ++++++++++ tests/auth.spec.js | 18 +++++++++++++++++- tests/evcc.js | 24 ++++++++++++++++-------- util/auth/auth.go | 15 +++++++++++++-- 7 files changed, 70 insertions(+), 14 deletions(-) diff --git a/cmd/flags.go b/cmd/flags.go index 3e977b933..6f5b36364 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -11,6 +11,9 @@ const ( flagIgnoreDatabase = "ignore-db" flagIgnoreDatabaseDescription = "Run command ignoring service database" + flagDisableAuth = "disable-auth" + flagDisableAuthDescription = "Disable authentication (dangerous)" + flagBatteryMode = "battery-mode" flagBatteryModeDescription = "Set battery mode (normal, hold, charge)" flagBatteryModeWait = "battery-mode-wait" diff --git a/cmd/root.go b/cmd/root.go index 4fb4ef7ea..bd331f5c0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -18,6 +18,7 @@ import ( "github.com/evcc-io/evcc/server" "github.com/evcc-io/evcc/server/updater" "github.com/evcc-io/evcc/util" + "github.com/evcc-io/evcc/util/auth" "github.com/evcc-io/evcc/util/config" "github.com/evcc-io/evcc/util/pipe" "github.com/evcc-io/evcc/util/sponsor" @@ -75,6 +76,8 @@ func init() { rootCmd.Flags().Bool("profile", false, "Expose pprof profiles") bind(rootCmd, "profile") + + rootCmd.Flags().Bool(flagDisableAuth, false, flagDisableAuthDescription) } // initConfig reads in config file and ENV variables if set @@ -270,7 +273,13 @@ func runRoot(cmd *cobra.Command, args []string) { // allow web access for vehicles configureAuth(conf.Network, config.Instances(config.Vehicles().Devices()), httpd.Router(), valueChan) - httpd.RegisterSystemHandler(valueChan, cache, func() { + auth := auth.New() + if ok, _ := cmd.Flags().GetBool(flagDisableAuth); ok { + log.WARN.Println("❗❗❗ Authentication is disabled. This is dangerous. Your data and credentials are not protected.") + auth.Disable() + } + + httpd.RegisterSystemHandler(valueChan, cache, auth, func() { log.INFO.Println("evcc was stopped by user. OS should restart the service. Or restart manually.") once.Do(func() { close(stopC) }) // signal loop to end }) diff --git a/server/http.go b/server/http.go index 8ade92212..68265d695 100644 --- a/server/http.go +++ b/server/http.go @@ -190,9 +190,8 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, valueChan chan<- util.Param) } // RegisterSystemHandler provides system level handlers -func (s *HTTPd) RegisterSystemHandler(valueChan chan<- util.Param, cache *util.Cache, shutdown func()) { +func (s *HTTPd) RegisterSystemHandler(valueChan chan<- util.Param, cache *util.Cache, auth auth.Auth, shutdown func()) { router := s.Server.Handler.(*mux.Router) - auth := auth.New() // api api := router.PathPrefix("/api").Subrouter() diff --git a/server/http_auth.go b/server/http_auth.go index 160bd2ebd..0b51e6757 100644 --- a/server/http_auth.go +++ b/server/http_auth.go @@ -73,6 +73,11 @@ func jwtFromRequest(r *http.Request) string { // authStatusHandler login status (true/false) based on jwt token. Error if admin password is not configured func authStatusHandler(auth auth.Auth) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + if auth.Disabled() { + w.Write([]byte("true")) + return + } + if !auth.IsAdminPasswordConfigured() { http.Error(w, "Not implemented", http.StatusNotImplemented) return @@ -130,6 +135,11 @@ func logoutHandler(w http.ResponseWriter, r *http.Request) { func ensureAuthHandler(auth auth.Auth) mux.MiddlewareFunc { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if auth.Disabled() { + next.ServeHTTP(w, r) + return + } + // check jwt token ok, err := auth.ValidateJwtToken(jwtFromRequest(r)) if !ok || err != nil { diff --git a/tests/auth.spec.js b/tests/auth.spec.js index 783b337b1..d81f03b30 100644 --- a/tests/auth.spec.js +++ b/tests/auth.spec.js @@ -89,7 +89,7 @@ test("http iframe hint", async ({ page }) => { }); test("update password", async ({ page }) => { - const instance = await start(BASIC, "password.sql"); + await start(BASIC, "password.sql"); await page.goto("/"); const oldPassword = "secret"; @@ -140,3 +140,19 @@ test("update password", async ({ page }) => { await stop(); }); + +test("disable auth", async ({ page }) => { + await start(BASIC, null, "--disable-auth"); + await page.goto("/"); + + // no password modal + const modal = page.getByTestId("password-modal"); + await expect(modal).not.toBeVisible(); + + // configuration page without login + await page.getByTestId("topnavigation-button").click(); + await page.getByRole("link", { name: "Configuration" }).click(); + await expect(page.getByRole("heading", { name: "Configuration" })).toBeVisible(); + + await stop(); +}); diff --git a/tests/evcc.js b/tests/evcc.js index a7d2c278f..686d7169a 100644 --- a/tests/evcc.js +++ b/tests/evcc.js @@ -42,12 +42,12 @@ function dbPath() { return path.join(os.tmpdir(), file); } -export async function start(config, sqlDumps) { +export async function start(config, sqlDumps, flags) { await _clean(); if (sqlDumps) { await _restoreDatabase(sqlDumps); } - return await _start(config); + return await _start(config, flags); } export async function stop(instance) { @@ -77,14 +77,15 @@ async function _restoreDatabase(sqlDumps) { } } -async function _start(config) { +async function _start(config, flags = []) { const configFile = config.includes("/") ? config : `tests/${config}`; const port = workerPort(); log(`wait until port ${port} is available`); // wait for port to be available await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true }); - log("starting evcc", { config, port }); - const instance = spawn(BINARY, ["--config", configFile], { + const additionalFlags = typeof flags === "string" ? [flags] : flags; + log("starting evcc", { config, port, additionalFlags }); + const instance = spawn(BINARY, ["--config", configFile, additionalFlags], { env: { EVCC_NETWORK_PORT: port.toString(), EVCC_DATABASE_DSN: dbPath() }, stdio: ["pipe", "pipe", "pipe"], }); @@ -108,10 +109,17 @@ async function _stop(instance) { log("evcc is down", { port }); return; } + // check if auth is required + const res = await axios.get(`${baseUrl()}/api/auth/status`); + log("auth status", res.status, res.statusText, res.data); + let cookie; + // login required + if (!res.data) { + const res = await axios.post(`${baseUrl()}/api/auth/login`, { password: "secret" }); + log("login", res.status, res.statusText); + cookie = res.headers["set-cookie"]; + } log("shutting down evcc", { port }); - const res = await axios.post(`${baseUrl()}/api/auth/login`, { password: "secret" }); - log(res.status, res.statusText); - const cookie = res.headers["set-cookie"]; await axios.post(`${baseUrl()}/api/system/shutdown`, {}, { headers: { cookie } }); log(`wait until port ${port} is closed`); await waitOn({ resources: [`tcp:${port}`], reverse: true, log: true }); diff --git a/util/auth/auth.go b/util/auth/auth.go index 1274137ef..a6cd4f7ef 100644 --- a/util/auth/auth.go +++ b/util/auth/auth.go @@ -22,18 +22,21 @@ type Auth interface { GenerateJwtToken(time.Duration) (string, error) ValidateJwtToken(string) (bool, error) IsAdminPasswordConfigured() bool + Disable() + Disabled() bool } type auth struct { settings settings.API + disabled bool } func New() Auth { - return &auth{settings: new(settings.Settings)} + return &auth{settings: new(settings.Settings), disabled: false} } func NewMock(settings settings.API) Auth { - return &auth{settings: settings} + return &auth{settings: settings, disabled: false} } func (a *auth) hashPassword(password string) (string, error) { @@ -140,3 +143,11 @@ func (a *auth) ValidateJwtToken(tokenString string) (bool, error) { return true, nil } + +func (a *auth) Disable() { + a.disabled = true +} + +func (a *auth) Disabled() bool { + return a.disabled +}