From 41508dd7d8ca96ecfaece62ef67846f0a38c510a Mon Sep 17 00:00:00 2001 From: Oleksii Milchenko Date: Thu, 17 Sep 2026 20:01:31 +0300 Subject: [PATCH] fix(server): ignore Basic auth header when AUTH_ENABLED=true OAuth2ProxyAuthentication turns any "Authorization: Basic user:pass" header into a user without checking the password. That was fine while nginx in config-ui verified the credentials against ADMIN_USER/ADMIN_PASS before proxying, and the lake only kept the username for audit fields. #8854 put auth.RequireAuth behind it. With AUTH_ENABLED=true that gate now accepts the unverified Basic user, so any username:password pair reaches every non-public endpoint (blueprints, connections, pipelines) on a lake that is exposed directly. #8880 closed the same hole for X-Forwarded-User with FORWARDED_USER_SECRET but left the Basic branch untouched. Only consult the Basic header when AUTH_ENABLED is off, which is the legacy nginx mode. With AUTH_ENABLED the lake authenticates callers itself: session cookie, API key, or forwarded headers with the shared secret, exactly what auth.go already logs as the supported set. Upstream: not yet submitted. --- backend/server/api/middlewares.go | 11 +++- .../server/api/middlewares_basicauth_test.go | 59 +++++++++++++++++++ .../api/middlewares_forwardsecret_test.go | 6 +- 3 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 backend/server/api/middlewares_basicauth_test.go diff --git a/backend/server/api/middlewares.go b/backend/server/api/middlewares.go index 567e3a9f41f..b460c676450 100644 --- a/backend/server/api/middlewares.go +++ b/backend/server/api/middlewares.go @@ -93,7 +93,14 @@ func getBasicAuthUserInfo(c *gin.Context, basicRes context.BasicRes) (*common.Us func OAuth2ProxyAuthentication(basicRes context.BasicRes) gin.HandlerFunc { logger := basicRes.GetLogger() - forwardedUserSecret := strings.TrimSpace(basicRes.GetConfigReader().GetString("FORWARDED_USER_SECRET")) + cfg := basicRes.GetConfigReader() + forwardedUserSecret := strings.TrimSpace(cfg.GetString("FORWARDED_USER_SECRET")) + // A Basic header is only an identity nginx (config-ui) has already verified + // against ADMIN_USER/ADMIN_PASS; the lake never checks the password. With + // AUTH_ENABLED the lake is the authenticator (session cookie, API key, or + // forwarded headers with the shared secret), so an unverified Basic header + // must not become a user, or any username:password passes RequireAuth. + trustBasicAuth := !(cfg.IsSet("AUTH_ENABLED") && cfg.GetBool("AUTH_ENABLED")) return func(c *gin.Context) { _, exist := c.Get(common.USER) if !exist { @@ -101,7 +108,7 @@ func OAuth2ProxyAuthentication(basicRes context.BasicRes) gin.HandlerFunc { if err != nil { logger.Warn(err, "rejected forwarded user headers") } - if user == nil || user.Name == "" { + if (user == nil || user.Name == "") && trustBasicAuth { // fetch with basic auth header user, err = getBasicAuthUserInfo(c, basicRes) if err != nil { diff --git a/backend/server/api/middlewares_basicauth_test.go b/backend/server/api/middlewares_basicauth_test.go new file mode 100644 index 00000000000..40b1c253b7e --- /dev/null +++ b/backend/server/api/middlewares_basicauth_test.go @@ -0,0 +1,59 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package api + +import ( + "encoding/base64" + "testing" + + "github.com/spf13/viper" +) + +func basicAuthHeader(username, password string) map[string]string { + return map[string]string{ + "Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password)), + } +} + +func TestOAuth2ProxyAuthenticationIgnoresBasicAuthWhenAuthEnabled(t *testing.T) { + cfg := viper.New() + cfg.Set("AUTH_ENABLED", true) + router := newProxyAuthRouterWithConfig(cfg) + body := performProxyAuthRequest(t, router, basicAuthHeader("anyone", "anything")) + if body.Authenticated { + t.Fatalf("expected Basic auth header to be ignored with AUTH_ENABLED=true, got %+v", body) + } +} + +func TestOAuth2ProxyAuthenticationTrustsBasicAuthWhenAuthDisabled(t *testing.T) { + cases := map[string]func(*viper.Viper){ + "AUTH_ENABLED unset": func(*viper.Viper) {}, + "AUTH_ENABLED explicit": func(cfg *viper.Viper) { cfg.Set("AUTH_ENABLED", false) }, + } + for name, configure := range cases { + t.Run(name, func(t *testing.T) { + cfg := viper.New() + configure(cfg) + router := newProxyAuthRouterWithConfig(cfg) + body := performProxyAuthRequest(t, router, basicAuthHeader("admin", "secret")) + if !body.Authenticated || body.Name != "admin" { + t.Fatalf("expected Basic auth user to be kept without AUTH_ENABLED, got %+v", body) + } + }) + } +} diff --git a/backend/server/api/middlewares_forwardsecret_test.go b/backend/server/api/middlewares_forwardsecret_test.go index 4b390aa526a..5b8063d40d5 100644 --- a/backend/server/api/middlewares_forwardsecret_test.go +++ b/backend/server/api/middlewares_forwardsecret_test.go @@ -57,9 +57,13 @@ type proxyAuthResponse struct { } func newProxyAuthRouter(secret string) *gin.Engine { - gin.SetMode(gin.TestMode) cfg := viper.New() cfg.Set("FORWARDED_USER_SECRET", secret) + return newProxyAuthRouterWithConfig(cfg) +} + +func newProxyAuthRouterWithConfig(cfg *viper.Viper) *gin.Engine { + gin.SetMode(gin.TestMode) basicRes := &proxyAuthTestBasicRes{ cfg: cfg, logger: logruslog.Global,