Support review bridge auth token
This commit is contained in:
parent
a383cf98d8
commit
d321364681
@ -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
|
||||
|
||||
@ -47,8 +47,11 @@ 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", "")),
|
||||
openClawGate: newOpenClawGatewayAdmissionGate(config),
|
||||
authService: service.NewStaticTokenAuthService(
|
||||
shared.EnvOrDefault("BRIDGE_AUTH_TOKEN", ""),
|
||||
shared.EnvOrDefault("BRIDGE_REVIEW_AUTH_TOKEN", ""),
|
||||
),
|
||||
openClawGate: newOpenClawGatewayAdmissionGate(config),
|
||||
taskForwarder: newDistributedTaskForwarder(distributedTaskForwarderConfig{
|
||||
Endpoint: resolveDistributedTaskForwardEndpoint(config),
|
||||
Token: resolveDistributedTaskForwardToken(config),
|
||||
|
||||
@ -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", "")
|
||||
|
||||
@ -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 "):]))
|
||||
}
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user