Support review bridge auth token

This commit is contained in:
Haitao Pan 2026-05-30 10:34:51 +08:00
parent a383cf98d8
commit d321364681
5 changed files with 58 additions and 14 deletions

View File

@ -40,15 +40,16 @@
环境变量:
- `BRIDGE_AUTH_TOKEN`
- `BRIDGE_REVIEW_AUTH_TOKEN`可选Apple review / beta 工测专用临时 token。清空该环境变量并重启/reload bridge 即可单独关停,不影响主 token。
- `ACP_ALLOWED_ORIGINS`
规则:
- `/acp``/acp/rpc` 都做 origin allowlist 校验
- 空 `Origin` 默认允许
- `/api/ping`、`/acp`、`/acp/rpc` 在 `BRIDGE_AUTH_TOKEN` 非空时都要求 bearer header
- `BRIDGE_AUTH_TOKEN` 为空时默认放行
- `BRIDGE_AUTH_TOKEN` 非空时,接受裸 token 或 `Bearer <token>`
- `/api/ping`、`/acp`、`/acp/rpc` 在任一 bridge token 非空时都要求 bearer header
- `BRIDGE_AUTH_TOKEN` `BRIDGE_REVIEW_AUTH_TOKEN`为空时默认放行
- token 非空时,接受裸 token 或 `Bearer <token>`
- `xworkmate-app` 生产 Origin 固定为 `https://xworkmate.svc.plus`
## 3.1 Lightweight Distributed Task Forwarding

View File

@ -47,7 +47,10 @@ func NewServer() *Server {
sessions: make(map[string]*session),
config: config,
allowedOrigins: shared.ParseAllowedOrigins(shared.EnvOrDefault("ACP_ALLOWED_ORIGINS", "https://xworkmate.svc.plus,http://localhost:*,http://127.0.0.1:*")),
authService: service.NewStaticTokenAuthService(shared.EnvOrDefault("BRIDGE_AUTH_TOKEN", "")),
authService: service.NewStaticTokenAuthService(
shared.EnvOrDefault("BRIDGE_AUTH_TOKEN", ""),
shared.EnvOrDefault("BRIDGE_REVIEW_AUTH_TOKEN", ""),
),
openClawGate: newOpenClawGatewayAdmissionGate(config),
taskForwarder: newDistributedTaskForwarder(distributedTaskForwarderConfig{
Endpoint: resolveDistributedTaskForwardEndpoint(config),

View File

@ -899,6 +899,27 @@ func TestHandleRPCCapabilitiesRequiresBearerAuthorizationWhenBridgeAuthTokenConf
}
}
func TestHandleRPCAllowsReviewBearerAuthorizationWhenConfigured(t *testing.T) {
t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-test-token")
t.Setenv("BRIDGE_REVIEW_AUTH_TOKEN", "review-bridge-test-token")
t.Setenv("BRIDGE_CONFIG_PATH", "../../example/config.yaml")
server := NewServer()
recorder := httptest.NewRecorder()
request := httptest.NewRequest(
http.MethodPost,
"http://127.0.0.1/acp/rpc",
strings.NewReader(`{"jsonrpc":"2.0","id":1,"method":"acp.capabilities"}`),
)
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Authorization", "Bearer review-bridge-test-token")
server.HandleRPC(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("expected 200 for configured review token, got %d", recorder.Code)
}
}
func TestHandleRPCRejectsUnknownOrigin(t *testing.T) {
t.Setenv("ACP_ALLOWED_ORIGINS", "https://xworkmate.svc.plus")
t.Setenv("BRIDGE_AUTH_TOKEN", "")

View File

@ -3,36 +3,42 @@ package service
import "strings"
type StaticTokenAuthService struct {
expectedToken string
expectedTokens map[string]struct{}
}
func NewStaticTokenAuthService(expectedToken string) *StaticTokenAuthService {
return &StaticTokenAuthService{
expectedToken: strings.TrimSpace(expectedToken),
func NewStaticTokenAuthService(expectedToken string, extraTokens ...string) *StaticTokenAuthService {
tokens := map[string]struct{}{}
for _, token := range append([]string{expectedToken}, extraTokens...) {
trimmed := strings.TrimSpace(token)
if trimmed != "" {
tokens[trimmed] = struct{}{}
}
}
return &StaticTokenAuthService{expectedTokens: tokens}
}
func (s *StaticTokenAuthService) ValidateToken(token string) bool {
token = strings.TrimSpace(token)
if s.expectedToken == "" {
if len(s.expectedTokens) == 0 {
return true
}
return token == s.expectedToken
_, ok := s.expectedTokens[token]
return ok
}
func (s *StaticTokenAuthService) ValidateAuthorizationHeader(header string) bool {
header = strings.TrimSpace(header)
if s.expectedToken == "" {
if len(s.expectedTokens) == 0 {
return true
}
if header == "" {
return false
}
if header == s.expectedToken {
if s.ValidateToken(header) {
return true
}
if !strings.HasPrefix(strings.ToLower(header), "bearer ") {
return false
}
return strings.TrimSpace(header[len("Bearer "):]) == s.expectedToken
return s.ValidateToken(strings.TrimSpace(header[len("Bearer "):]))
}

View File

@ -34,3 +34,16 @@ func TestStaticTokenAuthServiceValidateAuthorizationHeaderStrictWhenSet(t *testi
t.Fatal("expected non-bearer header to be rejected")
}
}
func TestStaticTokenAuthServiceValidateAuthorizationHeaderAcceptsReviewToken(t *testing.T) {
svc := NewStaticTokenAuthService("production-secret", "review-secret")
if !svc.ValidateAuthorizationHeader("Bearer production-secret") {
t.Fatal("expected production bearer header to be accepted")
}
if !svc.ValidateAuthorizationHeader("Bearer review-secret") {
t.Fatal("expected review bearer header to be accepted")
}
if svc.ValidateAuthorizationHeader("Bearer disabled-review-secret") {
t.Fatal("expected unconfigured review bearer header to be rejected")
}
}