Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ The main code repositories of Dubbo Admin include:
## Quick Start
Please refer to [official website](https://cn.dubbo.apache.org/zh-cn/overview/home/).

For the Kubernetes manifests, see [Kubernetes deployment](release/kubernetes/dubbo-system/README.md).

## Roadmap
Please refer to [RoadMap](https://github.com/apache/dubbo-admin/discussions/1300).

Expand Down
3 changes: 2 additions & 1 deletion app/dubbo-admin/dubbo-admin-oauth-example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ console:
user: admin
password: replace-with-admin-password
expirationTime: 3600
sessionSecret: replace-with-a-random-secret-at-least-32-bytes
# Required. Generate a unique value with: openssl rand -base64 32
sessionSecret: ""
sessionCookieSecure: true
providers:
github:
Expand Down
11 changes: 7 additions & 4 deletions app/dubbo-admin/dubbo-admin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,13 @@ console:
instance: http://101.34.253.152:30300/d/f5f48f75-13ec-489b-88ae-635ae38d8618?kiosk=1&theme=light
service: http://101.34.253.152:30300/d/b2e178fb-ada3-4d5e-9f54-de99e7f07662?kiosk=1&theme=light
# [Optional] typical user-pwd authentication, default user-pwd is admin-admin.
auth:
user: admin
password: dubbo@2025
expirationTime: 3600
auth:
user: admin
password: dubbo@2025
# Required. Set a deployment-specific random value, for example:
# openssl rand -base64 32
sessionSecret: ""
expirationTime: 3600

# [Optional] config for data storage, default is memory
store:
Expand Down
1 change: 1 addition & 0 deletions docs/server-develop.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ If you're using GoLand, you can run it locally by following steps:
2. Fill the block with the config that screenshot shows below:
![ide_configuration.png](./static/images/ide-config.png)
3. Modify the config file(app/dubbo-admin/dubbo-admin.yaml), make sure that the discovery, engine, store is configured.
Configure `console.auth.sessionSecret` with a unique random value before starting the server. For deployments that use a secret manager or Kubernetes Secret, the value can instead be provided through `DUBBO_ADMIN_SESSION_SECRET`. Generate a value with `openssl rand -base64 32`; the server refuses to start when the value is missing or shorter than 32 bytes.
Traffic-rule version history records RuleVersion audit entries for history, diff, and rollback material. Supported governance rule mutations fail closed if the version ledger is unavailable or cannot record the mutation.
4. Run the application, you can open the browser and visit localhost:8888/admin if everything works.

Expand Down
18 changes: 13 additions & 5 deletions pkg/config/app/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ var DefaultAdminConfig = func() AdminConfig {
Engine: engine.DefaultResourceEngineConfig(),
Observability: observability.DefaultObservabilityConfig(),
Diagnostics: diagnostics.DefaultDiagnosticsConfig(),
Console: console.DefaultConsoleConfig(),
EventBus: &eventBusCfg,
RuleVersioning: versioning.Default(),
}
Expand All @@ -79,7 +78,9 @@ func (c *AdminConfig) Sanitize() {
d.Sanitize()
}
c.Store.Sanitize()
c.Console.Sanitize()
if c.Console != nil {
c.Console.Sanitize()
}
c.Observability.Sanitize()
c.Diagnostics.Sanitize()
c.Log.Sanitize()
Expand All @@ -90,6 +91,9 @@ func (c *AdminConfig) Sanitize() {
}

func (c *AdminConfig) PreProcess() error {
if c.Console == nil {
return bizerror.New(bizerror.ConfigError, "console config is needed, but found empty")
}
discoveryPreProcess := func() error {
for _, d := range c.Discovery {
if err := d.PreProcess(); err != nil {
Expand All @@ -114,6 +118,9 @@ func (c *AdminConfig) PreProcess() error {
}

func (c *AdminConfig) PostProcess() error {
if c.Console == nil {
return bizerror.New(bizerror.ConfigError, "console config is needed, but found empty")
}
discoveryPostProcess := func() error {
for _, d := range c.Discovery {
if err := d.PostProcess(); err != nil {
Expand All @@ -138,6 +145,9 @@ func (c *AdminConfig) PostProcess() error {
}

func (c *AdminConfig) Validate() error {
if c.Console == nil {
return bizerror.New(bizerror.ConfigError, "console config is needed, but found empty")
}
if c.Log == nil {
c.Log = log.DefaultLogConfig()
} else if err := c.Log.Validate(); err != nil {
Expand All @@ -153,9 +163,7 @@ func (c *AdminConfig) Validate() error {
} else if err := c.Diagnostics.Validate(); err != nil {
return bizerror.Wrap(err, bizerror.ConfigError, "diagnostics config validation failed")
}
if c.Console == nil {
c.Console = console.DefaultConsoleConfig()
} else if err := c.Console.Validate(); err != nil {
if err := c.Console.Validate(); err != nil {
return bizerror.Wrap(err, bizerror.ConfigError, "console config validation failed")
}
if c.Observability == nil {
Expand Down
82 changes: 82 additions & 0 deletions pkg/config/app/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,15 @@
package app

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/apache/dubbo-admin/pkg/config"
configauth "github.com/apache/dubbo-admin/pkg/config/console/auth"
"github.com/apache/dubbo-admin/pkg/config/versioning"
)

Expand All @@ -35,3 +39,81 @@ func TestAdminConfigSanitizeRetainsDefaultRuleVersioning(t *testing.T) {
require.NotNil(t, cfg.RuleVersioning)
assert.Equal(t, versioning.DefaultMaxVersionsPerRule, cfg.RuleVersioning.MaxVersionsPerRule)
}

func TestAdminConfigValidateRejectsMissingConsole(t *testing.T) {
t.Setenv(configauth.SessionSecretEnvVar, "0123456789abcdef0123456789abcdef")
cfg := DefaultAdminConfig()

err := cfg.Validate()
require.ErrorContains(t, err, "console config is needed")
}

func TestConfigLoadFailsClosedForMissingOrEmptyConsole(t *testing.T) {
t.Setenv(configauth.SessionSecretEnvVar, "0123456789abcdef0123456789abcdef")
tests := []struct {
name string
contents string
wantError string
}{
{name: "missing", contents: "discovery: []\n", wantError: "console config is needed"},
{name: "null", contents: "console: null\n", wantError: "console config is needed"},
{name: "empty object", contents: "console: {}\n", wantError: "invalid gin mode"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
require.NoError(t, os.WriteFile(path, []byte(tt.contents), 0600))

cfg := DefaultAdminConfig()
err := config.Load(path, &cfg)
require.ErrorContains(t, err, tt.wantError)
})
}
}

func TestConfigLoadUsesSessionSecretFromEnvironment(t *testing.T) {
secret := "0123456789abcdef0123456789abcdef"
t.Setenv(configauth.SessionSecretEnvVar, secret)
path := filepath.Join(t.TempDir(), "config.yaml")
contents := `console:
ginMode: release
port: 8888
auth:
user: admin
password: test-password
expirationTime: 3600
sessionSecret: ""
discovery:
- id: test
name: test
type: mock
`
require.NoError(t, os.WriteFile(path, []byte(contents), 0600))

cfg := DefaultAdminConfig()
require.NoError(t, config.Load(path, &cfg))
assert.Equal(t, secret, cfg.Console.Auth.SessionSecret)
}

func TestConfigLoadRejectsMissingSessionSecret(t *testing.T) {
t.Setenv(configauth.SessionSecretEnvVar, "")
path := filepath.Join(t.TempDir(), "config.yaml")
contents := `console:
ginMode: release
port: 8888
auth:
user: admin
password: test-password
expirationTime: 3600
sessionSecret: ""
discovery:
- id: test
name: test
type: mock
`
require.NoError(t, os.WriteFile(path, []byte(contents), 0600))

cfg := DefaultAdminConfig()
err := config.Load(path, &cfg)
require.ErrorContains(t, err, "sessionSecret")
}
13 changes: 9 additions & 4 deletions pkg/config/console/auth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"fmt"
"net"
"net/url"
"os"
"regexp"
"slices"
"strings"
Expand All @@ -30,8 +31,9 @@ import (
)

const (
DefaultExpirationTime = 7200
DefaultSessionSecret = "secret"
DefaultExpirationTime = 7200
MinimumSessionSecretLength = 32
SessionSecretEnvVar = "DUBBO_ADMIN_SESSION_SECRET"

MethodPassword = "password"
ProviderTypeGitHub = "github"
Expand Down Expand Up @@ -89,8 +91,11 @@ func (c *Config) Validate() error {
if c.ExpirationTime <= 0 || c.ExpirationTime >= 24*60*60 {
return errors.New("auth: expirationTime should be greater than 0 and less than 86400")
}
if c.SessionSecret == "" {
c.SessionSecret = DefaultSessionSecret
if strings.TrimSpace(c.SessionSecret) == "" {
c.SessionSecret = os.Getenv(SessionSecretEnvVar)
}
if strings.TrimSpace(c.SessionSecret) == "" || len([]byte(c.SessionSecret)) < MinimumSessionSecretLength {
return fmt.Errorf("auth: sessionSecret must contain at least %d bytes", MinimumSessionSecretLength)
Comment thread
Copilot marked this conversation as resolved.
}
for id, provider := range c.Providers {
if err := validateProvider(id, &provider); err != nil {
Expand Down
42 changes: 38 additions & 4 deletions pkg/config/console/auth/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,53 @@ import (
)

func validConfig() *Config {
return &Config{User: "admin", Password: "secret", ExpirationTime: 3600}
return &Config{
User: "admin",
Password: "secret",
ExpirationTime: 3600,
SessionSecret: strings.Repeat("s", MinimumSessionSecretLength),
}
}

func TestConfigValidateDefaultsPasswordOnly(t *testing.T) {
func TestConfigValidatePasswordOnly(t *testing.T) {
cfg := validConfig()
if err := cfg.Validate(); err != nil {
t.Fatalf("Validate() error = %v", err)
}
if len(cfg.Methods) != 1 || cfg.Methods[0] != MethodPassword {
t.Fatalf("Methods = %v, want [%s]", cfg.Methods, MethodPassword)
}
if cfg.SessionSecret != DefaultSessionSecret {
t.Fatalf("SessionSecret = %q, want legacy default", cfg.SessionSecret)
if cfg.SessionSecret != strings.Repeat("s", MinimumSessionSecretLength) {
t.Fatalf("SessionSecret = %q, want configured secret", cfg.SessionSecret)
}
}

func TestConfigValidateRequiresSessionSecret(t *testing.T) {
t.Setenv(SessionSecretEnvVar, "")
cfg := validConfig()
cfg.SessionSecret = ""
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "sessionSecret") {
t.Fatalf("Validate() error = %v, want missing sessionSecret error", err)
}
}

func TestConfigValidateLoadsSessionSecretFromEnvironment(t *testing.T) {
t.Setenv(SessionSecretEnvVar, strings.Repeat("e", MinimumSessionSecretLength))
cfg := validConfig()
cfg.SessionSecret = ""
if err := cfg.Validate(); err != nil {
t.Fatalf("Validate() error = %v", err)
}
if cfg.SessionSecret != strings.Repeat("e", MinimumSessionSecretLength) {
t.Fatalf("SessionSecret = %q, want environment value", cfg.SessionSecret)
}
}

func TestConfigValidateRejectsShortSessionSecret(t *testing.T) {
cfg := validConfig()
cfg.SessionSecret = "short"
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "32 bytes") {
t.Fatalf("Validate() error = %v, want minimum sessionSecret length error", err)
}
}

Expand Down
5 changes: 0 additions & 5 deletions pkg/config/console/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,6 @@ func (c *Config) Validate() error {
if err := c.Auth.Validate(); err != nil {
return err
}
// Release deployments with external providers must reject the legacy default session secret and other short cookie-signing keys.
if c.GinMode == ReleaseMode && len(c.Auth.Providers) > 0 && len([]byte(c.Auth.SessionSecret)) < 32 {
return bizerror.New(bizerror.ConfigError, "auth sessionSecret must contain at least 32 bytes when providers are enabled in release mode")
}
return nil
}

Expand Down Expand Up @@ -136,7 +132,6 @@ func DefaultConsoleConfig() *Config {
User: "admin",
Password: "admin",
ExpirationTime: 3600,
SessionSecret: auth.DefaultSessionSecret,
},
}
}
21 changes: 18 additions & 3 deletions pkg/config/console/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
)

func TestReleaseProviderRequiresStrongSessionSecret(t *testing.T) {
t.Setenv(auth.SessionSecretEnvVar, "")
cfg := DefaultConsoleConfig()
cfg.Auth.Providers = map[string]auth.ProviderConfig{
"github": {
Expand All @@ -43,6 +44,19 @@ func TestReleaseProviderRequiresStrongSessionSecret(t *testing.T) {
}
}

func TestPasswordAuthenticationRequiresStrongSessionSecret(t *testing.T) {
t.Setenv(auth.SessionSecretEnvVar, "")
cfg := DefaultConsoleConfig()
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "sessionSecret") {
t.Fatalf("Validate() error = %v, want sessionSecret error", err)
}

cfg.Auth.SessionSecret = "a-long-deployment-specific-session-secret"
if err := cfg.Validate(); err != nil {
t.Fatalf("Validate() with strong secret error = %v", err)
}
}

func TestReleaseProviderRejectsShortSessionSecret(t *testing.T) {
cfg := DefaultConsoleConfig()
cfg.Auth.Providers = map[string]auth.ProviderConfig{
Expand All @@ -57,16 +71,17 @@ func TestReleaseProviderRejectsShortSessionSecret(t *testing.T) {
}
}

func TestDebugProviderAllowsLegacySessionSecret(t *testing.T) {
func TestDebugProviderRejectsShortSessionSecret(t *testing.T) {
cfg := DefaultConsoleConfig()
cfg.GinMode = DebugMode
cfg.Auth.SessionSecret = "short"
cfg.Auth.Providers = map[string]auth.ProviderConfig{
"github": {
Type: auth.ProviderTypeGitHub, ClientID: "id", ClientSecret: "secret",
RedirectURL: "http://localhost:8888/api/v1/auth/providers/github/callback", PostLoginRedirectURL: "http://localhost:8881/admin/",
},
}
if err := cfg.Validate(); err != nil {
t.Fatalf("Validate() error = %v", err)
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "32 bytes") {
t.Fatalf("Validate() error = %v, want minimum sessionSecret length error", err)
}
}
2 changes: 2 additions & 0 deletions pkg/config/display_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ import (

"github.com/apache/dubbo-admin/pkg/config"
"github.com/apache/dubbo-admin/pkg/config/app"
"github.com/apache/dubbo-admin/pkg/config/console"
configauth "github.com/apache/dubbo-admin/pkg/config/console/auth"
)

func TestConfigForDisplaySanitizesConsoleAuthenticationSecrets(t *testing.T) {
cfg := app.DefaultAdminConfig()
cfg.Console = console.DefaultConsoleConfig()
cfg.Console.Auth.Password = "password-secret"
cfg.Console.Auth.SessionSecret = "session-secret"
cfg.Console.Auth.Providers = map[string]configauth.ProviderConfig{
Expand Down
Loading
Loading