mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-11-04 16:40:21 +00:00
feat(auth): allow JWT IP changes by configuration (#1236)
* feat(auth): allow JWT IP changes by configuration * fix(auth): make JWT IP change setting concurrency-safe * test(auth): move JWT IP change setting tests out of MCP test file
This commit is contained in:
1 parent
8cb86e3853
commit
d19952a834
8 files changed
+172
-2
No files matched your search
@@ -61,6 +61,9 @@ type ConfigDashboard struct {
|
||||
AdminTemplate string `koanf:"admin_template" json:"admin_template,omitempty"`
|
||||
|
||||
EnablePlainIPInNotification bool `koanf:"enable_plain_ip_in_notification" json:"enable_plain_ip_in_notification,omitempty"` // 通知信息IP不打码
|
||||
// AllowJWTIPChange disables the IP binding check for browser login sessions.
|
||||
// Keep this disabled by default so a stolen JWT remains bound to its issuing IP.
|
||||
AllowJWTIPChange bool `koanf:"allow_jwt_ip_change" json:"allow_jwt_ip_change,omitempty"`
|
||||
|
||||
EnableMCP bool `koanf:"enable_mcp" json:"enable_mcp,omitempty"` // 是否启用 MCP 入口(默认关闭;启用前请审视 PAT scope/whitelist)
|
||||
|
||||
@@ -103,6 +106,10 @@ type Config struct {
|
||||
// SettingResponse 按值拷贝 ConfigDashboard 触发 copylocks。
|
||||
mcpEnabled atomic.Bool `koanf:"-" json:"-" yaml:"-"`
|
||||
|
||||
// jwtIPChangeAllowed mirrors AllowJWTIPChange for concurrent authentication
|
||||
// and setting updates. Keep atomic fields outside ConfigDashboard.
|
||||
jwtIPChangeAllowed atomic.Bool `koanf:"-" json:"-" yaml:"-"`
|
||||
|
||||
// oauth2 配置
|
||||
Oauth2 map[string]*Oauth2Config `koanf:"oauth2" json:"oauth2,omitempty"`
|
||||
|
||||
@@ -225,6 +232,8 @@ func (c *Config) Read(path string, frontendTemplates []FrontendTemplate) error {
|
||||
c.JWTTimeout = 1
|
||||
}
|
||||
|
||||
c.jwtIPChangeAllowed.Store(c.AllowJWTIPChange)
|
||||
|
||||
if c.AgentSecretKey == "" {
|
||||
c.AgentSecretKey, err = utils.GenerateRandomString(32)
|
||||
if err != nil {
|
||||
@@ -252,6 +261,17 @@ func (c *Config) SetMCPEnabled(v bool) {
|
||||
c.mcpEnabled.Store(v)
|
||||
}
|
||||
|
||||
// JWTIPChangeAllowed reports whether JWT sessions may change IP concurrently.
|
||||
func (c *Config) JWTIPChangeAllowed() bool {
|
||||
return c.jwtIPChangeAllowed.Load()
|
||||
}
|
||||
|
||||
// SetJWTIPChangeAllowed updates only the atomic mirror. save() synchronizes
|
||||
// the public field for persistence; authentication must use the accessor.
|
||||
func (c *Config) SetJWTIPChangeAllowed(v bool) {
|
||||
c.jwtIPChangeAllowed.Store(v)
|
||||
}
|
||||
|
||||
// Save 保存配置文件
|
||||
func (c *Config) Save() error {
|
||||
return c.save()
|
||||
@@ -320,6 +340,7 @@ func (c *Config) patchYAMLField(key string, value any) error {
|
||||
|
||||
func (c *Config) save() error {
|
||||
c.EnableMCP = c.mcpEnabled.Load()
|
||||
c.AllowJWTIPChange = c.jwtIPChangeAllowed.Load()
|
||||
data, err := yaml.Marshal(c)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -24,6 +24,7 @@ func TestReadConfig(t *testing.T) {
|
||||
{"user_template", c.UserTemplate, c.UserTemplate == "user-dist"},
|
||||
{"admin_template", c.AdminTemplate, c.AdminTemplate == "admin-dist"},
|
||||
{"agent_secret_key", c.AgentSecretKey, c.AgentSecretKey != ""},
|
||||
{"allow_jwt_ip_change", c.AllowJWTIPChange, !c.AllowJWTIPChange},
|
||||
}
|
||||
|
||||
for _, field := range testFields {
|
||||
@@ -36,7 +37,7 @@ func TestReadConfig(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("ReadFile", func(t *testing.T) {
|
||||
const testCfg = "jwt_secret_key: test\nuser_template: um\nadmin_template: am\nagent_secret_key: none\nsite_name: lowkick"
|
||||
const testCfg = "jwt_secret_key: test\nuser_template: um\nadmin_template: am\nagent_secret_key: none\nsite_name: lowkick\nallow_jwt_ip_change: true"
|
||||
|
||||
var testFrontendTemplates = []FrontendTemplate{
|
||||
{Path: "um"},
|
||||
@@ -59,6 +60,7 @@ func TestReadConfig(t *testing.T) {
|
||||
{"admin_template", c.AdminTemplate, c.AdminTemplate == "am"},
|
||||
{"agent_secret_key", c.AgentSecretKey, c.AgentSecretKey == "none"},
|
||||
{"site_name", c.SiteName, c.SiteName == "lowkick"},
|
||||
{"allow_jwt_ip_change", c.AllowJWTIPChange, c.AllowJWTIPChange},
|
||||
}
|
||||
|
||||
for _, field := range testFields {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJWTIPChangeAllowedPersistence(t *testing.T) {
|
||||
c := &Config{}
|
||||
if c.JWTIPChangeAllowed() {
|
||||
t.Fatal("zero-value Config must bind JWT sessions to their IP")
|
||||
}
|
||||
// An absent agent secret makes Read save the config during initialization.
|
||||
path := newTempConfig(t, "jwt_secret_key: test\nallow_jwt_ip_change: true\n")
|
||||
if err := c.Read(path, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !c.JWTIPChangeAllowed() {
|
||||
t.Fatal("Read must initialize the atomic mirror before saving")
|
||||
}
|
||||
for _, want := range []bool{true, false} {
|
||||
c.SetJWTIPChangeAllowed(want)
|
||||
if err := c.Save(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reloaded := &Config{}
|
||||
if err := reloaded.Read(path, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.AllowJWTIPChange != want || reloaded.JWTIPChangeAllowed() != want {
|
||||
t.Fatalf("saved and reloaded setting must be %v", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTIPChangeAllowedConcurrentAccess(t *testing.T) {
|
||||
c := &Config{filePath: filepath.Join(t.TempDir(), "config.yaml")}
|
||||
var wg sync.WaitGroup
|
||||
wg.Go(func() {
|
||||
for i := 0; i < 10000; i++ {
|
||||
c.SetJWTIPChangeAllowed(i%2 == 0)
|
||||
}
|
||||
})
|
||||
wg.Go(func() {
|
||||
for i := 0; i < 10000; i++ {
|
||||
_ = c.JWTIPChangeAllowed()
|
||||
}
|
||||
})
|
||||
for i := 0; i < 20; i++ {
|
||||
if err := c.Save(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -19,6 +19,7 @@ type SettingForm struct {
|
||||
AgentTLS bool `json:"tls,omitempty" validate:"optional"`
|
||||
EnableIPChangeNotification bool `json:"enable_ip_change_notification,omitempty" validate:"optional"`
|
||||
EnablePlainIPInNotification bool `json:"enable_plain_ip_in_notification,omitempty" validate:"optional"`
|
||||
AllowJWTIPChange *bool `json:"allow_jwt_ip_change,omitempty" validate:"optional"`
|
||||
EnableMCP *bool `json:"enable_mcp,omitempty" validate:"optional"`
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user