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=