This is an automated email from the ASF dual-hosted git repository.

Startrekzky pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new f03b83df5 fix: harden dbt pipeline inputs (#8878)
f03b83df5 is described below

commit f03b83df5897831eac527abc8879045d86bb2d8c
Author: Klesh Wong <[email protected]>
AuthorDate: Sun May 17 17:14:38 2026 +0800

    fix: harden dbt pipeline inputs (#8878)
    
    * fix: harden dbt pipeline inputs
    
    Co-authored-by: Copilot <[email protected]>
    
    * fix(test): disable auth for local e2e server
    
    Co-authored-by: Copilot <[email protected]>
    
    ---------
    
    Co-authored-by: Klesh Wong <[email protected]>
    Co-authored-by: Copilot <[email protected]>
---
 backend/helpers/oidchelper/config.go               |  24 ++--
 backend/helpers/oidchelper/config_test.go          |  47 ++++++++
 backend/plugins/dbt/dbt.go                         |   1 -
 backend/plugins/dbt/impl/impl.go                   |  18 ++-
 backend/plugins/dbt/tasks/convertor.go             |   1 +
 backend/plugins/dbt/tasks/git.go                   |  24 ++--
 backend/plugins/dbt/tasks/options.go               | 132 +++++++++++++++++++++
 backend/plugins/dbt/tasks/options_test.go          | 110 +++++++++++++++++
 backend/plugins/dbt/tasks/task_data.go             |   3 +
 backend/server/api/auth/auth.go                    |  16 +--
 backend/server/api/auth/middleware.go              |   8 +-
 backend/test/helper/client.go                      |   4 +
 .../components/advanced-editor/example/dbt.ts      |   2 +-
 env.example                                        |  10 +-
 14 files changed, 365 insertions(+), 35 deletions(-)

diff --git a/backend/helpers/oidchelper/config.go 
b/backend/helpers/oidchelper/config.go
index 4b45dfa3b..477e80591 100644
--- a/backend/helpers/oidchelper/config.go
+++ b/backend/helpers/oidchelper/config.go
@@ -88,20 +88,28 @@ func (c *Config) ProviderNames() []string {
 }
 
 // LoadConfig reads auth env vars via Viper and validates required fields.
-// Returns Config{AuthEnabled:false} when AUTH_ENABLED=false (the default,
-// preserves historical behavior).
+// AUTH_ENABLED defaults to true unless it is explicitly set to false.
 func LoadConfig(basicRes context.BasicRes) (*Config, error) {
        cfg := basicRes.GetConfigReader()
 
-       if !cfg.GetBool("AUTH_ENABLED") {
+       authEnabled := true
+       if cfg.IsSet("AUTH_ENABLED") {
+               authEnabled = cfg.GetBool("AUTH_ENABLED")
+       }
+       if !authEnabled {
                return &Config{AuthEnabled: false}, nil
        }
 
+       oidcEnabled := cfg.GetBool("OIDC_ENABLED")
        sessionSecret := strings.TrimSpace(cfg.GetString("SESSION_SECRET"))
-       if sessionSecret == "" {
-               return nil, fmt.Errorf("AUTH_ENABLED=true but SESSION_SECRET is 
not set")
-       }
-       if len(sessionSecret) < 32 {
+       if oidcEnabled {
+               if sessionSecret == "" {
+                       return nil, fmt.Errorf("OIDC_ENABLED=true but 
SESSION_SECRET is not set")
+               }
+               if len(sessionSecret) < 32 {
+                       return nil, fmt.Errorf("SESSION_SECRET must be at least 
32 bytes")
+               }
+       } else if sessionSecret != "" && len(sessionSecret) < 32 {
                return nil, fmt.Errorf("SESSION_SECRET must be at least 32 
bytes")
        }
 
@@ -121,7 +129,7 @@ func LoadConfig(basicRes context.BasicRes) (*Config, error) 
{
 
        out := &Config{
                AuthEnabled:    true,
-               OIDCEnabled:    cfg.GetBool("OIDC_ENABLED"),
+               OIDCEnabled:    oidcEnabled,
                Providers:      map[string]*ProviderConfig{},
                LogoutRedirect: cfg.GetBool("OIDC_LOGOUT_REDIRECT"),
                SessionSecret:  []byte(sessionSecret),
diff --git a/backend/helpers/oidchelper/config_test.go 
b/backend/helpers/oidchelper/config_test.go
index 1924fbb53..cc1d75f2c 100644
--- a/backend/helpers/oidchelper/config_test.go
+++ b/backend/helpers/oidchelper/config_test.go
@@ -20,6 +20,13 @@ package oidchelper
 import (
        "reflect"
        "testing"
+
+       "github.com/spf13/viper"
+
+       "github.com/apache/incubator-devlake/core/config"
+       corectx "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/core/dal"
+       "github.com/apache/incubator-devlake/core/log"
 )
 
 func TestParseScopes(t *testing.T) {
@@ -84,3 +91,43 @@ func TestProviderNamesSorted(t *testing.T) {
                t.Errorf("ProviderNames = %v, want sorted [entra google]", 
names)
        }
 }
+
+type basicResStub struct {
+       cfg config.ConfigReader
+}
+
+func (b basicResStub) GetConfigReader() config.ConfigReader { return b.cfg }
+func (b basicResStub) GetConfig(string) string              { return "" }
+func (b basicResStub) GetLogger() log.Logger                { return nil }
+func (b basicResStub) NestedLogger(string) corectx.BasicRes { return nil }
+func (b basicResStub) ReplaceLogger(log.Logger) corectx.BasicRes {
+       return nil
+}
+func (b basicResStub) GetDal() dal.Dal { return nil }
+
+func TestLoadConfigDefaultsAuthEnabled(t *testing.T) {
+       v := viper.New()
+
+       cfg, err := LoadConfig(basicResStub{cfg: v})
+       if err != nil {
+               t.Fatalf("LoadConfig returned error: %v", err)
+       }
+       if !cfg.AuthEnabled {
+               t.Fatal("AuthEnabled should default to true when AUTH_ENABLED 
is unset")
+       }
+       if cfg.OIDCEnabled {
+               t.Fatal("OIDCEnabled should default to false")
+       }
+       if len(cfg.SessionSecret) != 0 {
+               t.Fatalf("SessionSecret = %q, want empty when OIDC is 
disabled", string(cfg.SessionSecret))
+       }
+}
+
+func TestLoadConfigRequiresSessionSecretForOIDC(t *testing.T) {
+       v := viper.New()
+       v.Set("OIDC_ENABLED", true)
+
+       if _, err := LoadConfig(basicResStub{cfg: v}); err == nil {
+               t.Fatal("LoadConfig should reject OIDC-enabled config without 
SESSION_SECRET")
+       }
+}
diff --git a/backend/plugins/dbt/dbt.go b/backend/plugins/dbt/dbt.go
index ffb435ddc..02d055a9d 100644
--- a/backend/plugins/dbt/dbt.go
+++ b/backend/plugins/dbt/dbt.go
@@ -28,7 +28,6 @@ var PluginEntry impl.Dbt
 // standalone mode for debugging
 func main() {
        dbtCmd := &cobra.Command{Use: "dbt"}
-       _ = dbtCmd.MarkFlagRequired("projectPath")
        projectPath := dbtCmd.Flags().StringP("projectPath", "p", 
"/Users/abeizn/demoapp", "user dbt project directory.")
        projectGitURL := dbtCmd.Flags().StringP("projectGitURL", "g", "", "user 
dbt project git url.")
        projectName := dbtCmd.Flags().StringP("projectName", "n", "demoapp", 
"user dbt project name.")
diff --git a/backend/plugins/dbt/impl/impl.go b/backend/plugins/dbt/impl/impl.go
index 88eafa9f6..5da8799fd 100644
--- a/backend/plugins/dbt/impl/impl.go
+++ b/backend/plugins/dbt/impl/impl.go
@@ -18,6 +18,8 @@ limitations under the License.
 package impl
 
 import (
+       "fmt"
+
        "github.com/apache/incubator-devlake/core/dal"
        "github.com/apache/incubator-devlake/core/errors"
        "github.com/apache/incubator-devlake/core/plugin"
@@ -28,6 +30,7 @@ import (
 var _ interface {
        plugin.PluginMeta
        plugin.PluginTask
+       plugin.CloseablePluginTask
        plugin.PluginModel
 } = (*Dbt)(nil)
 
@@ -54,8 +57,8 @@ func (p Dbt) PrepareTaskData(taskCtx plugin.TaskContext, 
options map[string]inte
        if err != nil {
                return nil, err
        }
-       if op.ProjectPath == "" {
-               return nil, errors.Default.New("projectPath is required for dbt 
plugin")
+       if err := tasks.PrepareOptions(&op, 
taskCtx.GetConfig(tasks.DbtProjectBaseDirConfigKey)); err != nil {
+               return nil, err
        }
 
        if op.ProjectTarget == "" {
@@ -71,6 +74,17 @@ func (p Dbt) RootPkgPath() string {
        return "github.com/apache/incubator-devlake/plugins/dbt"
 }
 
+func (p Dbt) Close(taskCtx plugin.TaskContext) errors.Error {
+       data, ok := taskCtx.GetData().(*tasks.DbtTaskData)
+       if !ok || data == nil || data.Options == nil || 
!data.Options.ManagedProjectDir {
+               return nil
+       }
+       if err := tasks.CleanupManagedProjectDir(data.Options); err != nil {
+               return errors.Default.Wrap(err, fmt.Sprintf("cleanup dbt 
project path %q", data.Options.ProjectPath))
+       }
+       return nil
+}
+
 func (p Dbt) Name() string {
        return "dbt"
 }
diff --git a/backend/plugins/dbt/tasks/convertor.go 
b/backend/plugins/dbt/tasks/convertor.go
index 3d4ac56d3..a3a2d3f56 100644
--- a/backend/plugins/dbt/tasks/convertor.go
+++ b/backend/plugins/dbt/tasks/convertor.go
@@ -234,4 +234,5 @@ var DbtConverterMeta = plugin.SubTaskMeta{
        EntryPoint:       DbtConverter,
        EnabledByDefault: true,
        Description:      "Convert data by dbt",
+       Dependencies:     []*plugin.SubTaskMeta{&GitMeta},
 }
diff --git a/backend/plugins/dbt/tasks/git.go b/backend/plugins/dbt/tasks/git.go
index 49ea81f90..b43f3efcf 100644
--- a/backend/plugins/dbt/tasks/git.go
+++ b/backend/plugins/dbt/tasks/git.go
@@ -32,20 +32,26 @@ func Git(taskCtx plugin.SubTaskContext) errors.Error {
                return nil
        }
 
-       // clean ProjectPath
-       err := os.RemoveAll(data.Options.ProjectPath)
+       projectBaseDir, err := ensureProjectBaseDir(data.Options.ProjectBaseDir)
        if err != nil {
-               logger.Error(err, "cleanup before clone dbt project failed")
-               return errors.Convert(err)
+               logger.Error(err, "prepare dbt workspace failed")
+               return err
        }
+       projectPath, mkErr := os.MkdirTemp(projectBaseDir, "project-*")
+       if mkErr != nil {
+               logger.Error(mkErr, "create managed dbt project directory 
failed")
+               return errors.Convert(mkErr)
+       }
+       data.Options.ProjectPath = projectPath
 
-       // git clone from ProjectGitURL into ProjectPath
+       // git clone from ProjectGitURL into a managed temporary project 
directory
        cmd := exec.Command("git", "clone", data.Options.ProjectGitURL, 
data.Options.ProjectPath)
        logger.Info("start clone dbt project: %v", cmd)
-       out, err := cmd.CombinedOutput()
-       if err != nil {
-               logger.Error(err, "clone dbt project failed")
-               return errors.Convert(err)
+       out, cloneErr := cmd.CombinedOutput()
+       if cloneErr != nil {
+               _ = os.RemoveAll(data.Options.ProjectPath)
+               logger.Error(cloneErr, "clone dbt project failed")
+               return errors.Convert(cloneErr)
        }
        logger.Info("clone dbt project success: %v", string(out))
        return nil
diff --git a/backend/plugins/dbt/tasks/options.go 
b/backend/plugins/dbt/tasks/options.go
new file mode 100644
index 000000000..496290d43
--- /dev/null
+++ b/backend/plugins/dbt/tasks/options.go
@@ -0,0 +1,132 @@
+/*
+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 tasks
+
+import (
+       "net/url"
+       "os"
+       "path/filepath"
+       "strings"
+
+       "github.com/apache/incubator-devlake/core/errors"
+)
+
+const (
+       DbtProjectBaseDirConfigKey = "DBT_PROJECTS_DIR"
+       dbtProjectBaseDirName      = "devlake-dbt-projects"
+)
+
+func PrepareOptions(op *DbtOptions, configuredBaseDir string) errors.Error {
+       if op == nil {
+               return errors.Default.New("dbt options are required")
+       }
+
+       baseDir, err := normalizeBaseDir(configuredBaseDir)
+       if err != nil {
+               return err
+       }
+       op.ProjectBaseDir = baseDir
+       op.ProjectPath = strings.TrimSpace(op.ProjectPath)
+       op.ProjectGitURL = strings.TrimSpace(op.ProjectGitURL)
+
+       if op.ProjectGitURL != "" {
+               if err := validateProjectGitURL(op.ProjectGitURL); err != nil {
+                       return err
+               }
+               op.ProjectPath = ""
+               op.ManagedProjectDir = true
+               return nil
+       }
+
+       if op.ProjectPath == "" {
+               return errors.Default.New("projectPath is required for local 
dbt projects")
+       }
+
+       projectPath, err := normalizePathWithinBase(baseDir, op.ProjectPath)
+       if err != nil {
+               return err
+       }
+       op.ProjectPath = projectPath
+       return nil
+}
+
+func normalizeBaseDir(configuredBaseDir string) (string, errors.Error) {
+       baseDir := strings.TrimSpace(configuredBaseDir)
+       if baseDir == "" {
+               baseDir = filepath.Join(os.TempDir(), dbtProjectBaseDirName)
+       }
+       baseDir, err := filepath.Abs(filepath.Clean(baseDir))
+       if err != nil {
+               return "", errors.Convert(err)
+       }
+       return baseDir, nil
+}
+
+func normalizePathWithinBase(baseDir string, candidate string) (string, 
errors.Error) {
+       normalizedPath, err := filepath.Abs(filepath.Clean(candidate))
+       if err != nil {
+               return "", errors.Convert(err)
+       }
+       rel, err := filepath.Rel(baseDir, normalizedPath)
+       if err != nil {
+               return "", errors.Convert(err)
+       }
+       if rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) 
{
+               return "", errors.Default.New("projectPath must stay within " + 
baseDir)
+       }
+       return normalizedPath, nil
+}
+
+func validateProjectGitURL(rawURL string) errors.Error {
+       u, err := url.Parse(rawURL)
+       if err != nil {
+               return errors.Convert(err)
+       }
+       if u.Scheme != "https" && u.Scheme != "ssh" {
+               return errors.Default.New("projectGitURL must use https:// or 
ssh://")
+       }
+       if u.Host == "" {
+               return errors.Default.New("projectGitURL must include a 
hostname")
+       }
+       if u.Path == "" || u.Path == "/" {
+               return errors.Default.New("projectGitURL must include a 
repository path")
+       }
+       return nil
+}
+
+func ensureProjectBaseDir(baseDir string) (string, errors.Error) {
+       baseDir, err := normalizeBaseDir(baseDir)
+       if err != nil {
+               return "", err
+       }
+       if err := os.MkdirAll(baseDir, 0o755); err != nil {
+               return "", errors.Convert(err)
+       }
+       return baseDir, nil
+}
+
+func CleanupManagedProjectDir(op *DbtOptions) errors.Error {
+       if op == nil || !op.ManagedProjectDir || op.ProjectPath == "" {
+               return nil
+       }
+       projectPath, err := normalizePathWithinBase(op.ProjectBaseDir, 
op.ProjectPath)
+       if err != nil {
+               return err
+       }
+       return errors.Convert(os.RemoveAll(projectPath))
+}
diff --git a/backend/plugins/dbt/tasks/options_test.go 
b/backend/plugins/dbt/tasks/options_test.go
new file mode 100644
index 000000000..740e35273
--- /dev/null
+++ b/backend/plugins/dbt/tasks/options_test.go
@@ -0,0 +1,110 @@
+/*
+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 tasks
+
+import (
+       "os"
+       "path/filepath"
+       "testing"
+)
+
+func TestPrepareOptionsNormalizesLocalProjectPath(t *testing.T) {
+       baseDir := t.TempDir()
+       projectDir := filepath.Join(baseDir, "nested", "..", "project")
+
+       op := &DbtOptions{ProjectPath: projectDir}
+       if err := PrepareOptions(op, baseDir); err != nil {
+               t.Fatalf("PrepareOptions returned error: %v", err)
+       }
+
+       want := filepath.Join(baseDir, "project")
+       if op.ProjectPath != want {
+               t.Fatalf("ProjectPath = %q, want %q", op.ProjectPath, want)
+       }
+       if op.ProjectBaseDir != baseDir {
+               t.Fatalf("ProjectBaseDir = %q, want %q", op.ProjectBaseDir, 
baseDir)
+       }
+       if op.ManagedProjectDir {
+               t.Fatal("ManagedProjectDir should be false for local projects")
+       }
+}
+
+func TestPrepareOptionsRejectsPathTraversal(t *testing.T) {
+       baseDir := t.TempDir()
+       op := &DbtOptions{ProjectPath: filepath.Join(baseDir, "..", "outside")}
+
+       if err := PrepareOptions(op, baseDir); err == nil {
+               t.Fatal("PrepareOptions should reject projectPath outside the 
configured base directory")
+       }
+}
+
+func TestPrepareOptionsRejectsUnsafeGitURLs(t *testing.T) {
+       baseDir := t.TempDir()
+       cases := []string{
+               "file:///tmp/evil",
+               "../relative/repo.git",
+               "[email protected]:apache/incubator-devlake.git",
+       }
+
+       for _, rawURL := range cases {
+               t.Run(rawURL, func(t *testing.T) {
+                       op := &DbtOptions{ProjectGitURL: rawURL}
+                       if err := PrepareOptions(op, baseDir); err == nil {
+                               t.Fatalf("PrepareOptions should reject %q", 
rawURL)
+                       }
+               })
+       }
+}
+
+func TestPrepareOptionsAllowsManagedGitClone(t *testing.T) {
+       baseDir := t.TempDir()
+       op := &DbtOptions{
+               ProjectPath:   filepath.Join(baseDir, "ignored"),
+               ProjectGitURL: 
"https://github.com/apache/incubator-devlake.git";,
+       }
+
+       if err := PrepareOptions(op, baseDir); err != nil {
+               t.Fatalf("PrepareOptions returned error: %v", err)
+       }
+       if !op.ManagedProjectDir {
+               t.Fatal("ManagedProjectDir should be true for git-backed 
projects")
+       }
+       if op.ProjectPath != "" {
+               t.Fatalf("ProjectPath = %q, want empty for managed git clones", 
op.ProjectPath)
+       }
+}
+
+func TestCleanupManagedProjectDirRemovesManagedPath(t *testing.T) {
+       baseDir := t.TempDir()
+       projectDir := filepath.Join(baseDir, "clone")
+       if err := os.MkdirAll(projectDir, 0o755); err != nil {
+               t.Fatalf("MkdirAll returned error: %v", err)
+       }
+
+       op := &DbtOptions{
+               ProjectBaseDir:    baseDir,
+               ProjectPath:       projectDir,
+               ManagedProjectDir: true,
+       }
+       if err := CleanupManagedProjectDir(op); err != nil {
+               t.Fatalf("CleanupManagedProjectDir returned error: %v", err)
+       }
+       if _, err := os.Stat(projectDir); !os.IsNotExist(err) {
+               t.Fatalf("expected %q to be removed, stat err = %v", 
projectDir, err)
+       }
+}
diff --git a/backend/plugins/dbt/tasks/task_data.go 
b/backend/plugins/dbt/tasks/task_data.go
index d741954a9..00555fa2b 100644
--- a/backend/plugins/dbt/tasks/task_data.go
+++ b/backend/plugins/dbt/tasks/task_data.go
@@ -39,6 +39,9 @@ type DbtOptions struct {
        // deprecated, dbt run args
        Args  []string `json:"args"`
        Tasks []string `json:"tasks,omitempty"`
+
+       ProjectBaseDir    string `json:"-"`
+       ManagedProjectDir bool   `json:"-"`
 }
 
 type DbtTaskData struct {
diff --git a/backend/server/api/auth/auth.go b/backend/server/api/auth/auth.go
index e1012083e..ea7029a38 100644
--- a/backend/server/api/auth/auth.go
+++ b/backend/server/api/auth/auth.go
@@ -84,7 +84,7 @@ var (
 )
 
 // Init builds the default Service from env config and starts the background
-// loops. Panics if AUTH_ENABLED=true but the config is incomplete.
+// loops. Panics if auth is enabled but the config is incomplete.
 func Init(basicRes corectx.BasicRes) {
        initOnce.Do(func() {
                s, err := NewService(stdctx.Background(), basicRes)
@@ -357,13 +357,15 @@ func (s *Service) Logout(c *gin.Context) {
        }
 
        var sessionProvider string
-       if raw, err := c.Cookie(oidchelper.SessionCookieName); err == nil && 
raw != "" {
-               if claims, err := oidchelper.ParseSession(s.cfg.SessionSecret, 
raw); err == nil && claims.ID != "" {
-                       sessionProvider = claims.Provider
-                       if err := RevokeSession(s.db, claims.ID); err != nil {
-                               s.logger.Error(err, "auth: revoke session row")
+       if len(s.cfg.SessionSecret) > 0 {
+               if raw, err := c.Cookie(oidchelper.SessionCookieName); err == 
nil && raw != "" {
+                       if claims, err := 
oidchelper.ParseSession(s.cfg.SessionSecret, raw); err == nil && claims.ID != 
"" {
+                               sessionProvider = claims.Provider
+                               if err := RevokeSession(s.db, claims.ID); err 
!= nil {
+                                       s.logger.Error(err, "auth: revoke 
session row")
+                               }
+                               s.revoked.Add(claims.ID)
                        }
-                       s.revoked.Add(claims.ID)
                }
        }
        oidchelper.ClearSessionCookie(c, s.cfg)
diff --git a/backend/server/api/auth/middleware.go 
b/backend/server/api/auth/middleware.go
index ef39cabf7..231f4d546 100644
--- a/backend/server/api/auth/middleware.go
+++ b/backend/server/api/auth/middleware.go
@@ -65,6 +65,10 @@ func (s *Service) OIDCAuthentication() gin.HandlerFunc {
                        c.Next()
                        return
                }
+               if len(s.cfg.SessionSecret) == 0 {
+                       c.Next()
+                       return
+               }
                raw, err := c.Cookie(oidchelper.SessionCookieName)
                if err != nil || raw == "" {
                        c.Next()
@@ -92,8 +96,8 @@ func (s *Service) OIDCAuthentication() gin.HandlerFunc {
        }
 }
 
-// RequireAuth is the terminal gate. No-op when AUTH_ENABLED=false so existing
-// deployments are unaffected.
+// RequireAuth is the terminal gate. It only becomes a no-op when
+// AUTH_ENABLED=false is set explicitly.
 func (s *Service) RequireAuth() gin.HandlerFunc {
        return func(c *gin.Context) {
                if s.cfg == nil || !s.cfg.AuthEnabled {
diff --git a/backend/test/helper/client.go b/backend/test/helper/client.go
index 8c5c9d22a..c3855f589 100644
--- a/backend/test/helper/client.go
+++ b/backend/test/helper/client.go
@@ -137,6 +137,10 @@ func ConnectLocalServer(t *testing.T, clientConfig 
*LocalClientConfig) *DevlakeC
        fmt.Printf("Using test temp directory: %s\n", throwawayDir)
        logger := logruslog.Global.Nested("test")
        cfg := config.GetConfig()
+       // E2E helpers issue direct API requests without session or API-key 
auth.
+       // Keep local test servers aligned with that contract.
+       t.Setenv("AUTH_ENABLED", "false")
+       cfg.Set("AUTH_ENABLED", false)
        cfg.Set("DB_URL", clientConfig.DbURL)
        db, err := runner.NewGormDb(cfg, logger)
        require.NoError(t, err)
diff --git 
a/config-ui/src/routes/blueprint/detail/components/advanced-editor/example/dbt.ts
 
b/config-ui/src/routes/blueprint/detail/components/advanced-editor/example/dbt.ts
index 6b3117e2d..c09a3a228 100644
--- 
a/config-ui/src/routes/blueprint/detail/components/advanced-editor/example/dbt.ts
+++ 
b/config-ui/src/routes/blueprint/detail/components/advanced-editor/example/dbt.ts
@@ -21,7 +21,7 @@ const dbt = [
     {
       plugin: 'dbt',
       options: {
-        projectPath: '/var/www/html/my-project',
+        projectPath: '/tmp/devlake-dbt-projects/my-project',
         projectGitURL: '',
         projectName: 'myproject',
         projectTarget: 'dev',
diff --git a/env.example b/env.example
index 1cfd04a6b..64fa6b39f 100755
--- a/env.example
+++ b/env.example
@@ -93,10 +93,10 @@ 
ENABLE_SUBTASKS_BY_DEFAULT="jira:collectIssueChangelogs:true,jira:extractIssueCh
 ##########################
 # OIDC / Authentication
 ##########################
-# Master switch. When false (default) DevLake behaves as before: API keys for
-# /rest/* and trust X-Forwarded-User from an upstream proxy. Set true to
-# require authentication on all non-whitelisted routes.
-AUTH_ENABLED=false
+# Master switch. Auth is enabled by default; set false only for isolated local
+# development. When enabled without OIDC, DevLake accepts API keys for /rest/*
+# and can trust X-Forwarded-User from an upstream proxy.
+AUTH_ENABLED=true
 
 # OIDC user login. Requires AUTH_ENABLED=true.
 OIDC_ENABLED=false
@@ -142,7 +142,7 @@ OIDC_GOOGLE_DISPLAY_NAME=Google
 # can also sign the user out at the IdP.
 OIDC_LOGOUT_REDIRECT=false
 
-# Required when AUTH_ENABLED=true. At least 32 bytes of high-entropy data.
+# Required when OIDC_ENABLED=true. At least 32 bytes of high-entropy data.
 # Used to sign session JWTs (HS256) and to derive the AES-GCM key that
 # encrypts the OIDC state cookie. Rotating this invalidates all sessions.
 SESSION_SECRET=

Reply via email to