Phase 3+4: 共享 HTTP 客户端 + admin API + audit log

Phase 3 (httpclient):
- shared Client (Timeout + MaxResponseBytes 截断)
- SSRF: 16 段私网 CIDR (含 169.254/::ffff:0:0/96 IPv4-mapped)
  + hostname allowlist (精确/后缀/*. 通配)
  + scheme 校验 (仅 http/https)
- auth 适配器: none / simple (YARN user.name) / basic (SetBasicAuth)
- DoWithRedirect: 跨主机跳转保留 Authorization, max 5 默认
- 19 个测试全绿 (httptest 模拟)

Phase 4 (admin + audit):
- internal/audit: Entry + Repo.Insert/List + MarshalDetails, snake_case JSON
- internal/middleware: AdminAuth/AgentAuth (constant-time 比对)
- internal/admin: 6 端点 (GET/POST/PUT/DELETE /admin/clusters, GET /admin/audit)
  + 写操作触发 audit_log (含 before/after diff)
  + AuthPassword 空=保留旧密码
  + 校验: 必填字段 + auth_type 枚举 + rate_limit>=0
- main.go: 挂载 storage.Open + admin.Mount
- L fix: audit.Entry 加 json tag (smoke test 发现大写 key 不规范)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
tao.chen
2026-07-10 13:54:24 +08:00
co-authored by Claude
parent a4e2472716
commit 2c39091706
14 changed files with 1528 additions and 2 deletions
+53
View File
@@ -0,0 +1,53 @@
package httpclient
import (
"errors"
"fmt"
"net/http"
"net/url"
"spark-mcp-go/internal/cluster"
)
// ApplyAuth rewrites req according to the cluster's configured AuthType.
// - none / empty: no-op
// - simple: appends ?user.name=<username> (YARN SimpleAuth)
// - basic: attaches HTTP Basic credentials
//
// baseURL is kept for future use (e.g. reconstructing absolute URLs in Phase 5)
// and is intentionally unused in this version.
func ApplyAuth(req *http.Request, baseURL string, c *cluster.Cluster) error {
if baseURL == "" {
// baseURL is reserved for Phase 5 host rewriting.
}
switch c.AuthType {
case cluster.AuthNone, "":
return nil
case cluster.AuthSimple:
u, err := url.Parse(req.URL.String())
if err != nil {
return fmt.Errorf("httpclient: simple auth parse url: %w", err)
}
q := u.Query()
user := c.AuthUsername
if user == "" {
user = "yarn"
}
q.Set("user.name", user)
u.RawQuery = q.Encode()
req.URL = u
return nil
case cluster.AuthBasic:
if c.AuthUsername == "" || c.AuthPassword == "" {
return errors.New("httpclient: basic auth requires username and password")
}
req.SetBasicAuth(c.AuthUsername, c.AuthPassword)
return nil
default:
return fmt.Errorf("httpclient: unknown auth type %q", c.AuthType)
}
}